Euterpea 2.0.0 → 2.0.1
raw patch · 227 files changed
+1802/−1853 lines, 227 filessetup-changedbinary-addedPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
Files
- .git/COMMIT_EDITMSG +1/−4
- .git/FETCH_HEAD +1/−1
- .git/ORIG_HEAD +1/−1
- .git/index binary
- .git/logs/HEAD +10/−21
- .git/logs/refs/heads/master +7/−16
- .git/logs/refs/remotes/origin/HEAD +1/−1
- .git/logs/refs/remotes/origin/master +5/−16
- .git/objects/07/34c378fd990e9ab7a7f9fb2fe361a84d41e95e binary
- .git/objects/08/1fa266eebe94ad9d1401de24163f05d6d378b3 binary
- .git/objects/08/cf131880f52ce6a6d1e8dd2f0afb8ee0f30bcf binary
- .git/objects/09/53e4337fcc90c2a5c2ec957f9d46b4aa32ca99 binary
- .git/objects/0c/262c7a72c9e17768fa826a90472e376c09767a binary
- .git/objects/0e/40a7618f3033863399b53fd837057d05c90d33 binary
- .git/objects/0e/668119469c51796e6d599be3210067a29b2519 binary
- .git/objects/0f/40bd8185ca44ac857d80e28f1571f7dc4c2f0b binary
- .git/objects/10/aedf5be4cd4cba55b64d5f6995b2848da5156b binary
- .git/objects/12/422ea17098ee8ef49c69ec4f706610531ff7a5 binary
- .git/objects/13/ff9346b625728e873af12be8d41ba411d16d3a binary
- .git/objects/15/3be0645392d0fcd358365100287c8153667803 binary
- .git/objects/16/053d551af0017fc14257241ace886edd2afc02 binary
- .git/objects/18/120c7891df6d6b7a4edb6505c550b9a48dcfc6 binary
- .git/objects/18/2a0398ba6a070c251b036c128fb0d923b1b04a binary
- .git/objects/19/0569154271005cf561a49b93e0dacf5d237670 binary
- .git/objects/19/a4d9c645f7e4b9f5414e5bb1b45de9a1c3ea40 binary
- .git/objects/1b/963cb16e69a4719c07d01041419fc0016888e9 binary
- .git/objects/1d/4d0774da205784863ae2d06988d4c525dd5b22 binary
- .git/objects/1d/865ab036c3dafe78300b3876f46e7fc91b1836 binary
- .git/objects/1e/e46d0b56ddaa82b9a081a21369c1509b84402e binary
- .git/objects/1f/0b51995096b04b9c0bb36b633333ee53cf6ede binary
- .git/objects/22/9b691fbf867126dc4e40ed8b5bf16facc90e7a binary
- .git/objects/23/e8522114db4fb8526a9948e7c91250c63a9928 binary
- .git/objects/26/252acd178a04d8b98a75016a754d8b7bc4dfdd binary
- .git/objects/26/d1e77943b1fe7cb446dd7ac0fef7cc1c495845 +6/−0
- .git/objects/27/f49fd3429321b1ee2c1546eb63256dc0730c1f binary
- .git/objects/28/0bbfa003c4249f5fc4a59ad5fa724272bc2d02 binary
- .git/objects/28/fb2df2b4bf2f8be62602e7b3b6ad99289fefac binary
- .git/objects/29/09458fe91e193322f78abdafd7c8b9c2480b9f binary
- .git/objects/29/3bd1d6053e165fb63a1bc860ce65d28160759f too large to diff
- .git/objects/2c/1360591bec55fca7893b298b53197be3c867ba binary
- .git/objects/2c/230b496eb8d7ee15595fc8bb6e0893af19008b binary
- .git/objects/2e/0b0c295239ecdf1e63e6a7536505ac813f47f9 binary
- .git/objects/31/5c172ba4c6d45564249a225f0e075f671c5fbb +0/−3
- .git/objects/31/ba3d0f73a9b2159cfa27d8e11e275daee8c481 binary
- .git/objects/32/3835bed7c98ee14e0445126cfc2df5cbed9bae binary
- .git/objects/32/eee8d97247283b82f847ae18b0739f135577c6 +1/−0
- .git/objects/35/05de91ee4ca4ab2c00ab4d0e05daae68dde53b binary
- .git/objects/35/8436995aff5176e643cebf3c5d070536ac57cb binary
- .git/objects/36/e8447f23fe4c9fc02644fae5965b7b9fe966fd binary
- .git/objects/37/3df86f01c69ae7dcc599f9c11e786415e4d1e6 binary
- .git/objects/38/79732a59fd96782ac7fec01e882bc419a650f6 binary
- .git/objects/39/1156a543887fc398238ebd6b61194807e1c8df binary
- .git/objects/3a/77dd1cc37ae544654eafe0d6106d0322fcd6d6 binary
- .git/objects/3a/f2137fd14e17ec39cd86c35eb817be41236fa3 binary
- .git/objects/3e/ee7432174ba6920cdc77a546f40abea4363bb0 binary
- .git/objects/40/0528d8d6e72ec2edc4fee57e208bc972fdc90f +2/−0
- .git/objects/40/d6be12def8fc357df4248d9943bd3cc1f75f89 +1/−0
- .git/objects/41/2224f723a4330a7004dc7c1eb1f62ddef2c206 binary
- .git/objects/41/d19a4642f6660a4179f3c71e91735989598daf binary
- .git/objects/42/4bc74689367dde97ffde93f82558e7dc6c0bf2 binary
- .git/objects/42/e3ec9572ed4138542efdf91a07554c835f4882 binary
- .git/objects/46/4199cbe596b6d8e366c3f5900c87740c819acf binary
- .git/objects/46/d74297ecb00ced0498fca2d37ef1123967d685 binary
- .git/objects/4d/011c28a383821201d0c2da48db909df774df62 binary
- .git/objects/4f/1c6161cd6b76930d6ab63910f661e4c9280c64 +0/−1
- .git/objects/50/3f8ef4dd7e6196ceb6045ca57275d8e337e43b +0/−3
- .git/objects/52/8e6cd0e3e16d9dbb927d3bb4bcd83ecf8dfad6 +0/−6
- .git/objects/52/c3e648f28c37c75cbad302221d9d7e63b8b83e binary
- .git/objects/52/e7bb8b5351858b6c18b368d3947dc1231a5f9f binary
- .git/objects/53/d8a923285953866fc263789d67e4c38525c4fa binary
- .git/objects/54/5758c9b40f03a253a47131d9207245c4986c51 binary
- .git/objects/54/7dcd3ab2847acaaa664560053063485e9799af +2/−0
- .git/objects/55/ec0b6acfed4cda746ba489cbfaa0a78a12330e binary
- .git/objects/57/c5213c0484db428f6c4cf6e6214e89a8ae1495 binary
- .git/objects/59/22faa1f075f43bf5ff192ef560a4c1ee5ac7d8 binary
- .git/objects/5a/0a4abb675bb7edeeb1581ef36c3107d9ddabc0 binary
- .git/objects/5a/7588690dec40ed5e79d4831b388b095febcb2f binary
- .git/objects/5a/db5e8db2e5525db664ff2e17de8b87ff0f8670 binary
- .git/objects/5b/f300951510805ff5a3fe166487788a031778a5 binary
- .git/objects/5e/a6aeb96d1894c8e38cc8f865fadb85bcacaad2 binary
- .git/objects/60/492b4c521313c25cb4c46b57d0b72cc74d7cbd binary
- .git/objects/61/d09a71180e5a1a4ea1d893f82a4709d8e79c27 binary
- .git/objects/64/0e75d56b3758363d58a86d728c5403aa92c840 binary
- .git/objects/64/5249b0b24a3bd4e03e6fc30606002c2e91696d +0/−1
- .git/objects/66/08033e85b06e8aa5ef9fe4a115b47500a073f0 binary
- .git/objects/66/a5ca12ace8806a4001b7082db3bdb8b35267eb binary
- .git/objects/67/eab6e41d9b7d3b8cce4cfc4b5ad44649fba760 binary
- .git/objects/68/440e1520af9fb733724b7b81e907a4dab956b8 binary
- .git/objects/68/afa542d4797b1b25cc5a255fb4ae50014fcc65 binary
- .git/objects/69/680bdab02d8e951979477178f796b43bd5df01 binary
- .git/objects/69/fdad39ac0a3e2e0c9b5347e7e6f666d8f769a4 binary
- .git/objects/6b/cae40f355478a4d15eee9b66d161e7a0a12dd2 binary
- .git/objects/6c/080e1fa918201ddbbdf7faa8bf8426e590092d binary
- .git/objects/6c/78ae03df6c1bc335f2c3d91ab317967dd10a8b binary
- .git/objects/6d/983efc33facf5d019fefdc11c253042f6f1a57 binary
- .git/objects/70/c3238c627e9fc1187b9828d5583a470e9ad056 binary
- .git/objects/72/00e3a6391ba70ec0e19bf5e6a7effdbcdd0cf6 binary
- .git/objects/72/420aa90b49de8264d659eee043ad94c94f08f0 binary
- .git/objects/72/44f2a719afed28d42fdd235e91ba80a76bf8c8 +0/−2
- .git/objects/73/bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c +0/−1
- .git/objects/76/62906e1fb8860b48907234e40addbfdc4bd604 binary
- .git/objects/79/e1abc7ad5a3aa84e9c723999da1a2977b07839 binary
- .git/objects/7a/7cacc4ba4fa2ca82e2d85abf6b44e7f93625ce binary
- .git/objects/7b/f4417449debcc857e7444b200faf64d6cb58a2 binary
- .git/objects/80/1751ee70ac4794f625f38c9417be1408ff90df binary
- .git/objects/80/c8d821ef0e1310670d9383a0164a161ef6d896 binary
- .git/objects/81/64db06d50157acee8946c245af5c82e93298f7 binary
- .git/objects/85/af43da1948ffbdf3b1fff153d88119842852b9 binary
- .git/objects/87/7cdbe88a44c2962bc9bc66e021ad8a9c185268 +0/−4
- .git/objects/88/37a8a5670a0e0375d119c9d20d4353b5913a03 binary
- .git/objects/88/3ee63aca3703b5c269e0fa4bf0880ade5a9984 binary
- .git/objects/89/367b0736b710c6e9c383ef2e019fb07f6cf51c binary
- .git/objects/8c/a262164429aa9765dfd2fa47331f3349869329 binary
- .git/objects/8f/926490ec9977aaa657c03492a2092cd96ed879 binary
- .git/objects/90/f10f0ae98fa550ed4f3ba96b2726b2fa0f5948 binary
- .git/objects/95/4c9cf3acd7bc7d436c9548c6d50a7936b3055c binary
- .git/objects/95/530bd9f8b33aa4403e449541aca8782fcf58d7 binary
- .git/objects/96/325c6d0739c4a2f762fb0d8e9f6de13202272d binary
- .git/objects/97/d0dbaf178d03770a923bdd241b477208b116bf binary
- .git/objects/98/70f262ba46f8c63923b4c9d373b5502d534bff binary
- .git/objects/9b/c107a80e863822b328bf64a795c3ef54e001b1 +0/−1
- .git/objects/9d/73156b57d5c16c87c54244219f4be0df7a4d9c binary
- .git/objects/9d/78fa8fa693d03da04df9e4e1644ed7a5b5d237 binary
- .git/objects/9e/ad253d2b2018bf4a7fb68a0470bb225793668e binary
- .git/objects/a1/b5f177c89e03bb1f90afbc3ee699d9c9b063de binary
- .git/objects/a1/bd5bad273b384e804013a76ba56070c418244b binary
- .git/objects/a2/e4ba96b68cfb636a59754dc68261df30cc6530 binary
- .git/objects/a2/f609f631d66e93917a5ace7ad8e9d3e1ff7dcd binary
- .git/objects/a6/92c6ff1f43e7f0e6048501efab53476b451697 binary
- .git/objects/a9/232c0b57605a3ad8a06faad4de4c75fde6ba1a binary
- .git/objects/a9/2acc695c112526737719cae217919dd26cc8b0 binary
- .git/objects/a9/e9e0080f3f5357b79e1034457f57c4c50a61c1 binary
- .git/objects/aa/6d02eafa38dd2e10940244f0951551753f3060 binary
- .git/objects/aa/72de41851f42d3f1023c01ae0edc5832c47ebd binary
- .git/objects/ab/dbdc6ad3b69bcd244b80ee836215e015658a56 binary
- .git/objects/ad/3a905fb36fe44c5f0f7d1bf0e6fab3d36b4d61 binary
- .git/objects/ad/790ff8bd9f52c7ddfb2b28093a8ff21940e3b6 binary
- .git/objects/ad/934e32017310e7df1888d183754ae30fa6c67e binary
- .git/objects/af/a69c96947b53fb4d8c3f13d2597f42f166b954 binary
- .git/objects/b0/9012e440f3043b48b074cdbcc2b592972bdf3f binary
- .git/objects/b1/030551f45c9a9ab195759fb1b77e5b346421fb binary
- .git/objects/b1/127851608df2ad2af6966e3558157bf768a183 binary
- .git/objects/b1/3d346f7417c0e401862c9a2473787ff583afd1 binary
- .git/objects/b2/4b2515b2d6b41f2101bf7c8ea8e825495d0168 binary
- .git/objects/b3/dac8fd881bc1fd8dfe20ab121cfa7344ace238 binary
- .git/objects/b6/af0dcb1cc69563bda463208ca520057e2e11b3 binary
- .git/objects/b7/d28caad2a4b8583bee99a026cabed13ed818d6 binary
- .git/objects/b8/4d39c532ad044ad8b60cee3bc5292061de39a6 binary
- .git/objects/b8/724b568863051d8deac9e814dc00174f5281a9 binary
- .git/objects/b9/499a0096c52a092b1f7dd5d9ebabfcaeff6f96 binary
- .git/objects/bc/3466a48bc1ea669750415e2741d93b7f244611 binary
- .git/objects/bc/7e70f191403e2d3c97a09b11bcf2476f038a60 binary
- .git/objects/be/dc2220c923720d40fbf6b318d4b16245c73a55 +0/−3
- .git/objects/be/ff554d699578a58c434cccfd0b09b8be1b872d binary
- .git/objects/c1/1295cd418389633d038186b951bd6617e7f6e4 binary
- .git/objects/c1/7ffce0743beb137344a74f6ae5fb5046c955b1 binary
- .git/objects/c1/ba0d1945a06d1610f61128dbe0355002d90c2f binary
- .git/objects/c4/3ce9302dcd23cedd4cf553e09c685e528e587d binary
- .git/objects/c5/fc3c5cebdf7ad00414095d73e7f628cd20d4b4 binary
- .git/objects/c7/3c5f5403087b409710374014d8d3f3b91489d1 binary
- .git/objects/c8/2f114a3376658176753aa69cb1882ff9e34024 binary
- .git/objects/c8/66ed10fcb19f4a2fb6382721b1d617638ba958 binary
- .git/objects/cc/07b21458a8fc31fe26df3ebb0b95a3fb3dabda binary
- .git/objects/cc/2e92fccfecefca2ba40533b089ace71f5bba65 binary
- .git/objects/cd/023c5127ad0de2f22259fb58b303ab534b75b4 binary
- .git/objects/cd/b16d7a4d7ce1ddb46f600a67259900ad69e165 binary
- .git/objects/ce/7bb09495716bdbad1408f0d4969fdc39100142 binary
- .git/objects/cf/2b919bc619dd9c7d058d324954872788d208e9 binary
- .git/objects/cf/aafd13c43ed1207a8a323d7532ac25ce643e36 binary
- .git/objects/cf/df673ee21dc78ee19e2f2c4403ff9c81ffa1f4 binary
- .git/objects/d0/ded1ec1f5916aa8e3df243a7a2e953e49abfb7 binary
- .git/objects/d2/ba530f2b786897d2b6b681d5f438efa154b349 binary
- .git/objects/d2/cf53ff238e7f97c925f41e114e7100ed259734 binary
- .git/objects/d3/9da1469f1f05215ea3284392a585730464e8de binary
- .git/objects/d6/32a8bbb2a5162fba6f031bf3a3a8c58167c034 binary
- .git/objects/d7/b27434f174e0dbe05f8bef916760dfee482141 binary
- .git/objects/d8/a75f45fc2a9f796ef5af34dd139f243f0e1df6 binary
- .git/objects/d9/712de374ecf26cff922a1fa3eaf739bfa71471 binary
- .git/objects/db/b2c0d96968285e950f91cef5c54071d6e1c387 binary
- .git/objects/dc/277dc4fd142e9bdf214b89824f0ecd5c5910ec binary
- .git/objects/de/0e0984670170b610524acf8c55af1102969233 binary
- .git/objects/de/184f0f48f080cd952b4906bd6d10ce95d657aa binary
- .git/objects/de/1cc98e65c7c80ceea57473bdadce3f65ea49a7 binary
- .git/objects/df/b22ee42cf26aa1d33211add3c6fdc1a8d117be binary
- .git/objects/e0/947bc61ee49f66c63fe1c930c899cbd6b1a368 binary
- .git/objects/e1/a32ab024ca47185d08c172e66b5ae83b6d8ea5 binary
- .git/objects/e2/bdf760d4a28556c3c8bdcec92c5cb51c376e8a binary
- .git/objects/e4/61c0ab2477605902ab7a9c42969603f38bd586 +2/−0
- .git/objects/e5/c124001e0bd84a9c3f6f42ee520c39fcba32cc binary
- .git/objects/e6/e7d977a11bc0f0fd2d821ba0faff4eab5e22bd binary
- .git/objects/e7/a64b8b7d8600b944a2d7a5ffff8f590b4199b7 binary
- .git/objects/ea/e7723c93fc4fd784831416d1f087bed2707fba binary
- .git/objects/eb/02119459572b6b0cf79a2f9b4895b3b9764380 binary
- .git/objects/eb/2a0d24372b9029567ea502f851b922edc3f462 binary
- .git/objects/eb/4dfc2eece871c74eeab502a3943f85884afdad binary
- .git/objects/ed/8ca5964cb76d44755cdcb3ed22fd070676ebc7 binary
- .git/objects/ed/fb8f1378c1c0021ed0a0f847618b4b310c14f1 +0/−3
- .git/objects/ef/337b7200de9c55fdc48320e19a26e19564d79b binary
- .git/objects/f2/2b65e167b5ea54b84305158d31368b9c10f0a7 binary
- .git/objects/f2/9871f63fcf1a8c2cc7ea8d7e2d3505f641c08e binary
- .git/objects/f2/bba23a1c3bbf6ff9964d90731485399a69b8a9 binary
- .git/objects/f4/077bc12e204876fb485cac9bb75ac11a651c0c binary
- .git/objects/f5/602af44f15d195f373445ff4ea9525fe659cdc binary
- .git/objects/f8/154e00ed87f700e71790ab9966fc8414807b8a +0/−2
- .git/objects/fd/70ab6de3c28ef42e6450fabade4e4bbdd5869e binary
- .git/objects/fd/7be762cd1a7bbca6b0eeec10ab260858db340d binary
- .git/objects/fe/09b1a7385815667ec69a1b68be676d0eb7cf46 binary
- .git/objects/fe/3a486f7a94b12d196d8e17a1fdb728e26dbff3 binary
- .git/objects/fe/977d416f9d343f9e97002d5664550eeed16ee0 binary
- .git/objects/pack/pack-070189755adac644d6a1c3fd2d044e268ceae11e.idx binary
- .git/objects/pack/pack-070189755adac644d6a1c3fd2d044e268ceae11e.pack binary
- .git/packed-refs +1/−1
- .git/refs/heads/master +1/−1
- .git/refs/remotes/origin/master +1/−1
- Control/Arrow/ArrowP.lhs +38/−38
- Control/SF/SF.lhs +113/−113
- Euterpea.cabal +3/−8
- Euterpea.lhs +19/−19
- Euterpea/IO/Audio/IO.hs +204/−204
- Euterpea/IO/Audio/Types.hs +51/−51
- Euterpea/IO/MIDI/ExportMidiFile.lhs +281/−281
- Euterpea/IO/MIDI/FromMidi.lhs +267/−267
- Euterpea/IO/MIDI/MEvent.lhs +11/−7
- Euterpea/IO/MIDI/MidiIO.lhs +665/−665
- License +20/−20
- ReadMe.txt +37/−37
- Setup.hs +50/−50
@@ -1,4 +1,1 @@-dependency caps and audio render update--Dependencies are capped more strictly and audio rendering will now clip-by default (rather than overflowing) when values exceed +/-1.+Dependency versions narrowed again
@@ -1,1 +1,1 @@-896621f361866ce787d0bad1dfd5434d371b5679 branch 'master' of https://github.com/Euterpea/Euterpea2+40d6be12def8fc357df4248d9943bd3cc1f75f89 branch 'master' of https://github.com/Euterpea/Euterpea2
@@ -1,1 +1,1 @@-b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85+aa72de41851f42d3f1023c01ae0edc5832c47ebd
binary file changed (2340 → 2340 bytes)
@@ -1,21 +1,10 @@-0000000000000000000000000000000000000000 f8154e00ed87f700e71790ab9966fc8414807b8a donya <donyavq@netscape.net> 1450296215 -0500 commit (initial): Initial commit-f8154e00ed87f700e71790ab9966fc8414807b8a 4f1c6161cd6b76930d6ab63910f661e4c9280c64 donya <donyavq@netscape.net> 1450311885 -0500 commit: Renaming & restructuring-4f1c6161cd6b76930d6ab63910f661e4c9280c64 85af43da1948ffbdf3b1fff153d88119842852b9 donya <donyavq@netscape.net> 1450315255 -0500 commit: Field renaming for MContext-85af43da1948ffbdf3b1fff153d88119842852b9 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 donya <donyavq@netscape.net> 1451596992 -0500 pull --progress --rebase --prune origin master: checkout 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2-5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 donya <donyavq@netscape.net> 1451596993 -0500 rebase finished: returning to refs/heads/master-5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 8ca262164429aa9765dfd2fa47331f3349869329 donya <donyavq@netscape.net> 1451597251 -0500 commit: Dependency on UISF removed-8ca262164429aa9765dfd2fa47331f3349869329 9bc107a80e863822b328bf64a795c3ef54e001b1 donya <donyavq@netscape.net> 1451597503 -0500 commit: Reame updated-9bc107a80e863822b328bf64a795c3ef54e001b1 31ba3d0f73a9b2159cfa27d8e11e275daee8c481 donya <donyavq@netscape.net> 1451954740 -0500 pull --progress --rebase --prune origin master: checkout 31ba3d0f73a9b2159cfa27d8e11e275daee8c481-31ba3d0f73a9b2159cfa27d8e11e275daee8c481 31ba3d0f73a9b2159cfa27d8e11e275daee8c481 donya <donyavq@netscape.net> 1451954740 -0500 rebase finished: returning to refs/heads/master-31ba3d0f73a9b2159cfa27d8e11e275daee8c481 877cdbe88a44c2962bc9bc66e021ad8a9c185268 Donya Quick <donyavq@netscape.net> 1452131260 -0500 commit: Added writeWav-877cdbe88a44c2962bc9bc66e021ad8a9c185268 323835bed7c98ee14e0445126cfc2df5cbed9bae Donya Quick <donyavq@netscape.net> 1452451734 -0500 pull --progress --rebase --prune origin master: checkout 323835bed7c98ee14e0445126cfc2df5cbed9bae-323835bed7c98ee14e0445126cfc2df5cbed9bae 323835bed7c98ee14e0445126cfc2df5cbed9bae Donya Quick <donyavq@netscape.net> 1452451735 -0500 rebase finished: returning to refs/heads/master-323835bed7c98ee14e0445126cfc2df5cbed9bae 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c Donya Quick <donyavq@netscape.net> 1452537818 -0500 pull --progress --rebase --prune origin master: checkout 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c-73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c Donya Quick <donyavq@netscape.net> 1452537818 -0500 rebase finished: returning to refs/heads/master-73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 Donya Quick <donyavq@netscape.net> 1453084077 -0500 commit: removeInstruments fixed-61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 Donya Quick <donyavq@netscape.net> 1455221540 -0500 commit: invert fix-b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd Donya Quick <donyavq@netscape.net> 1456862557 -0500 pull --progress --rebase --prune origin master: checkout 086557985556cc77f5a559d74d0502a2d761babd-086557985556cc77f5a559d74d0502a2d761babd 086557985556cc77f5a559d74d0502a2d761babd Donya Quick <donyavq@netscape.net> 1456862558 -0500 rebase finished: returning to refs/heads/master-086557985556cc77f5a559d74d0502a2d761babd 7a5e865406dc8dc8d2bd730b7625656fc4c80036 Donya Quick <donyavq@netscape.net> 1459959169 -0400 commit: Dependency version changes-7a5e865406dc8dc8d2bd730b7625656fc4c80036 896621f361866ce787d0bad1dfd5434d371b5679 Donya Quick <donyavq@netscape.net> 1461339370 -0400 commit: Exports added-896621f361866ce787d0bad1dfd5434d371b5679 4afd6ed7d81c474bf5c88c395a584e4f68b039ce Donya Quick <donyavq@netscape.net> 1462819240 -0400 commit: dependency caps and audio render update+0000000000000000000000000000000000000000 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 donya <donyavq@netscape.net> 1453085786 -0500 clone: from https://github.com/Euterpea/Euterpea2.git+61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 donya <donyavq@netscape.net> 1455231002 -0500 pull --progress --rebase --prune origin master: checkout b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85+b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 donya <donyavq@netscape.net> 1455231002 -0500 rebase finished: returning to refs/heads/master+b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd donya <donyavq@netscape.net> 1456848084 -0500 commit: ToMusic1 instance for AbsPitch added+086557985556cc77f5a559d74d0502a2d761babd e461c0ab2477605902ab7a9c42969603f38bd586 donya <donyavq@netscape.net> 1465925605 -0500 pull --progress --rebase --prune origin master: checkout e461c0ab2477605902ab7a9c42969603f38bd586+e461c0ab2477605902ab7a9c42969603f38bd586 e461c0ab2477605902ab7a9c42969603f38bd586 donya <donyavq@netscape.net> 1465925606 -0500 rebase finished: returning to refs/heads/master+e461c0ab2477605902ab7a9c42969603f38bd586 aa72de41851f42d3f1023c01ae0edc5832c47ebd donya <donyavq@netscape.net> 1465930431 -0500 commit: Dependency versions temporarily relaxed+aa72de41851f42d3f1023c01ae0edc5832c47ebd 40d6be12def8fc357df4248d9943bd3cc1f75f89 donya <donyavq@netscape.net> 1466564399 -0500 pull --progress --rebase --prune origin master: checkout 40d6be12def8fc357df4248d9943bd3cc1f75f89+40d6be12def8fc357df4248d9943bd3cc1f75f89 40d6be12def8fc357df4248d9943bd3cc1f75f89 donya <donyavq@netscape.net> 1466564400 -0500 rebase finished: returning to refs/heads/master+40d6be12def8fc357df4248d9943bd3cc1f75f89 fd7be762cd1a7bbca6b0eeec10ab260858db340d donya <donyavq@netscape.net> 1467320139 -0500 commit: Dependency versions narrowed again
@@ -1,16 +1,7 @@-0000000000000000000000000000000000000000 f8154e00ed87f700e71790ab9966fc8414807b8a donya <donyavq@netscape.net> 1450296215 -0500 commit (initial): Initial commit-f8154e00ed87f700e71790ab9966fc8414807b8a 4f1c6161cd6b76930d6ab63910f661e4c9280c64 donya <donyavq@netscape.net> 1450311885 -0500 commit: Renaming & restructuring-4f1c6161cd6b76930d6ab63910f661e4c9280c64 85af43da1948ffbdf3b1fff153d88119842852b9 donya <donyavq@netscape.net> 1450315255 -0500 commit: Field renaming for MContext-85af43da1948ffbdf3b1fff153d88119842852b9 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 donya <donyavq@netscape.net> 1451596992 -0500 rebase finished: refs/heads/master onto 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2-5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 8ca262164429aa9765dfd2fa47331f3349869329 donya <donyavq@netscape.net> 1451597251 -0500 commit: Dependency on UISF removed-8ca262164429aa9765dfd2fa47331f3349869329 9bc107a80e863822b328bf64a795c3ef54e001b1 donya <donyavq@netscape.net> 1451597503 -0500 commit: Reame updated-9bc107a80e863822b328bf64a795c3ef54e001b1 31ba3d0f73a9b2159cfa27d8e11e275daee8c481 donya <donyavq@netscape.net> 1451954740 -0500 rebase finished: refs/heads/master onto 31ba3d0f73a9b2159cfa27d8e11e275daee8c481-31ba3d0f73a9b2159cfa27d8e11e275daee8c481 877cdbe88a44c2962bc9bc66e021ad8a9c185268 Donya Quick <donyavq@netscape.net> 1452131260 -0500 commit: Added writeWav-877cdbe88a44c2962bc9bc66e021ad8a9c185268 323835bed7c98ee14e0445126cfc2df5cbed9bae Donya Quick <donyavq@netscape.net> 1452451735 -0500 rebase finished: refs/heads/master onto 323835bed7c98ee14e0445126cfc2df5cbed9bae-323835bed7c98ee14e0445126cfc2df5cbed9bae 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c Donya Quick <donyavq@netscape.net> 1452537818 -0500 rebase finished: refs/heads/master onto 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c-73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 Donya Quick <donyavq@netscape.net> 1453084077 -0500 commit: removeInstruments fixed-61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 Donya Quick <donyavq@netscape.net> 1455221540 -0500 commit: invert fix-b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd Donya Quick <donyavq@netscape.net> 1456862558 -0500 rebase finished: refs/heads/master onto 086557985556cc77f5a559d74d0502a2d761babd-086557985556cc77f5a559d74d0502a2d761babd 7a5e865406dc8dc8d2bd730b7625656fc4c80036 Donya Quick <donyavq@netscape.net> 1459959169 -0400 commit: Dependency version changes-7a5e865406dc8dc8d2bd730b7625656fc4c80036 896621f361866ce787d0bad1dfd5434d371b5679 Donya Quick <donyavq@netscape.net> 1461339370 -0400 commit: Exports added-896621f361866ce787d0bad1dfd5434d371b5679 4afd6ed7d81c474bf5c88c395a584e4f68b039ce Donya Quick <donyavq@netscape.net> 1462819240 -0400 commit: dependency caps and audio render update+0000000000000000000000000000000000000000 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 donya <donyavq@netscape.net> 1453085786 -0500 clone: from https://github.com/Euterpea/Euterpea2.git+61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 donya <donyavq@netscape.net> 1455231002 -0500 rebase finished: refs/heads/master onto b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85+b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd donya <donyavq@netscape.net> 1456848084 -0500 commit: ToMusic1 instance for AbsPitch added+086557985556cc77f5a559d74d0502a2d761babd e461c0ab2477605902ab7a9c42969603f38bd586 donya <donyavq@netscape.net> 1465925606 -0500 rebase finished: refs/heads/master onto e461c0ab2477605902ab7a9c42969603f38bd586+e461c0ab2477605902ab7a9c42969603f38bd586 aa72de41851f42d3f1023c01ae0edc5832c47ebd donya <donyavq@netscape.net> 1465930431 -0500 commit: Dependency versions temporarily relaxed+aa72de41851f42d3f1023c01ae0edc5832c47ebd 40d6be12def8fc357df4248d9943bd3cc1f75f89 donya <donyavq@netscape.net> 1466564400 -0500 rebase finished: refs/heads/master onto 40d6be12def8fc357df4248d9943bd3cc1f75f89+40d6be12def8fc357df4248d9943bd3cc1f75f89 fd7be762cd1a7bbca6b0eeec10ab260858db340d donya <donyavq@netscape.net> 1467320139 -0500 commit: Dependency versions narrowed again
@@ -1,1 +1,1 @@-0000000000000000000000000000000000000000 9bc107a80e863822b328bf64a795c3ef54e001b1 Donya Quick <donyavq@netscape.net> 1451691193 -0800 clone: from https://github.com/Euterpea/Euterpea2.git+0000000000000000000000000000000000000000 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 donya <donyavq@netscape.net> 1453085786 -0500 clone: from https://github.com/Euterpea/Euterpea2.git
@@ -1,16 +1,5 @@-0000000000000000000000000000000000000000 f8154e00ed87f700e71790ab9966fc8414807b8a donya <donyavq@netscape.net> 1450296225 -0500 update by push-f8154e00ed87f700e71790ab9966fc8414807b8a 4f1c6161cd6b76930d6ab63910f661e4c9280c64 donya <donyavq@netscape.net> 1450311899 -0500 update by push-4f1c6161cd6b76930d6ab63910f661e4c9280c64 85af43da1948ffbdf3b1fff153d88119842852b9 donya <donyavq@netscape.net> 1450315267 -0500 update by push-85af43da1948ffbdf3b1fff153d88119842852b9 5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 donya <donyavq@netscape.net> 1451596991 -0500 pull --progress --rebase --prune origin master: fast-forward-5ea6aeb96d1894c8e38cc8f865fadb85bcacaad2 8ca262164429aa9765dfd2fa47331f3349869329 donya <donyavq@netscape.net> 1451597262 -0500 update by push-8ca262164429aa9765dfd2fa47331f3349869329 9bc107a80e863822b328bf64a795c3ef54e001b1 donya <donyavq@netscape.net> 1451597514 -0500 update by push-9bc107a80e863822b328bf64a795c3ef54e001b1 31ba3d0f73a9b2159cfa27d8e11e275daee8c481 donya <donyavq@netscape.net> 1451954738 -0500 pull --progress --rebase --prune origin master: fast-forward-31ba3d0f73a9b2159cfa27d8e11e275daee8c481 877cdbe88a44c2962bc9bc66e021ad8a9c185268 Donya Quick <donyavq@netscape.net> 1452131278 -0500 update by push-877cdbe88a44c2962bc9bc66e021ad8a9c185268 323835bed7c98ee14e0445126cfc2df5cbed9bae Donya Quick <donyavq@netscape.net> 1452451733 -0500 pull --progress --rebase --prune origin master: fast-forward-323835bed7c98ee14e0445126cfc2df5cbed9bae 73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c Donya Quick <donyavq@netscape.net> 1452537816 -0500 pull --progress --rebase --prune origin master: fast-forward-73bc3b4c2d2ca70c8a8844a118e3d3db3aeb4d0c 61d09a71180e5a1a4ea1d893f82a4709d8e79c27 Donya Quick <donyavq@netscape.net> 1453084100 -0500 update by push-61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 Donya Quick <donyavq@netscape.net> 1455221559 -0500 update by push-b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd Donya Quick <donyavq@netscape.net> 1456862556 -0500 pull --progress --rebase --prune origin master: fast-forward-086557985556cc77f5a559d74d0502a2d761babd 7a5e865406dc8dc8d2bd730b7625656fc4c80036 Donya Quick <donyavq@netscape.net> 1459959184 -0400 update by push-7a5e865406dc8dc8d2bd730b7625656fc4c80036 896621f361866ce787d0bad1dfd5434d371b5679 Donya Quick <donyavq@netscape.net> 1461339414 -0400 update by push-896621f361866ce787d0bad1dfd5434d371b5679 4afd6ed7d81c474bf5c88c395a584e4f68b039ce Donya Quick <donyavq@netscape.net> 1462819260 -0400 update by push+61d09a71180e5a1a4ea1d893f82a4709d8e79c27 b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 donya <donyavq@netscape.net> 1455231001 -0500 pull --progress --rebase --prune origin master: fast-forward+b1eb4e6bd0c330b7cbe720d59df11bf3105b2b85 086557985556cc77f5a559d74d0502a2d761babd donya <donyavq@netscape.net> 1456848102 -0500 update by push+086557985556cc77f5a559d74d0502a2d761babd e461c0ab2477605902ab7a9c42969603f38bd586 donya <donyavq@netscape.net> 1465925602 -0500 pull --progress --rebase --prune origin master: fast-forward+e461c0ab2477605902ab7a9c42969603f38bd586 aa72de41851f42d3f1023c01ae0edc5832c47ebd donya <donyavq@netscape.net> 1465930440 -0500 update by push+aa72de41851f42d3f1023c01ae0edc5832c47ebd 40d6be12def8fc357df4248d9943bd3cc1f75f89 donya <donyavq@netscape.net> 1466564397 -0500 pull --progress --rebase --prune origin master: fast-forward
binary file changed (245 → absent bytes)
binary file changed (absent → 168 bytes)
binary file changed (absent → 1048 bytes)
binary file changed (absent → 1548 bytes)
binary file changed (absent → 233 bytes)
binary file changed (absent → 155285 bytes)
binary file changed (430 → absent bytes)
binary file changed (215 → absent bytes)
binary file changed (absent → 15321 bytes)
binary file changed (4571 → absent bytes)
binary file changed (absent → 10765 bytes)
binary file changed (137 → absent bytes)
binary file changed (absent → 188 bytes)
binary file changed (82 → absent bytes)
binary file changed (absent → 108 bytes)
binary file changed (245 → absent bytes)
binary file changed (828 → absent bytes)
binary file changed (absent → 4844 bytes)
binary file changed (245 → absent bytes)
binary file changed (absent → 579 bytes)
binary file changed (absent → 158 bytes)
binary file changed (2129 → absent bytes)
binary file changed (4712 → absent bytes)
binary file changed (136 → absent bytes)
binary file changed (2136 → absent bytes)
@@ -0,0 +1,6 @@+xTQoÚ@Þóý++OJ"J7:¡1i+tªZvÓ¦I½+]+\Xr)l¿¾vrPÄç³?Ûí³"Ó·çïÞäEZgn YVóimd¹|ø{e¥+Ýki>+=QeP¼Q±'Â'£ZndBGçD×*_E.×瘝Ðh&¬²©|U~×"SJ¦pQhSY4Ý$re0 tgOèÑÏ6OðàGQ8gdMõ³*KmÀÇðÜÜ2«SL"&Y^Í`4ü~¿]Í:nÈ"pBç:Þæ5pY Õ&Ø©ÆN÷^¿×û+Ío5:±ÛÈUË"ÊKSu¦îÁ¼ç?*¤óOi®4~V+âóo @ÅóDÄ"ã£z¬ ùÞêìý0\câź+Ë$<NûÑçn¤ÂAÔúá¥þõ·Nn×_òé©®îfÃåÆc¶ÆãÓ~eà¦4KÞļ²G¼4ÇØ±ëEK¸Ý$;±ÝthX«j;õà¹æí yø|N\JSZu@)µk~¤=VÝмMpN¥bû¿cõ.û¸ÂȶÑ{hÛ«p¿C¹m;»¸½+R©î±ÛÝKûLìöçS#ø$ÒJ©¿dÄ¡ÎvƧ»ð8÷èKn{é+ñ@
binary file changed (absent → 3699 bytes)
binary file changed (417 → absent bytes)
binary file changed (245 → absent bytes)
binary file changed (136 → absent bytes)
file too large to diff
binary file changed (2125 → absent bytes)
binary file changed (absent → 7054 bytes)
binary file changed (137 → absent bytes)
@@ -1,3 +0,0 @@-x+)JMU022e040031QpJ,ÎLÎLw+Í+ÖËÉ(f 8¹ yQðBÃ-E®Gfä"«-Ö*Ê©XÇ|?Gú°é§Ã7¥6O«½ÈÕ Uä_6já'Y¬=6Ýshë¤éÚ÷í¡ª<ýAÆ·aæ¨v.`î rãòç"¡-RóRR@ÔTµÎw±ÜØÙUÊUêÛ]}äþ]¨¢ÊT°{ÎnÌòyxwK~WºêLµSÕ_X2
binary file changed (172 → absent bytes)
binary file changed (159 → absent bytes)
@@ -0,0 +1,1 @@+x+)JMU°0g040031QH,É(w--I-*HMÔË(fP»ø¼Òyã¿-nw«üû~FÆ3ª891)1'>71¹(¿X/áö¦7334â¦òO<÷õ¨CáµÛÌJ(s
binary file changed (absent → 12169 bytes)
binary file changed (244 → absent bytes)
binary file changed (absent → 28935 bytes)
binary file changed (absent → 27398 bytes)
binary file changed (137 → absent bytes)
binary file changed (6732 → absent bytes)
binary file changed (83 → absent bytes)
binary file changed (246 → absent bytes)
binary file changed (1294 → absent bytes)
@@ -0,0 +1,2 @@+x+)JMU03e040031QvÓËÈdØQäÖÌ*Ûûêä;âþA++ò>i§>Lßú*d3«h¯¡Y÷Ël
@@ -0,0 +1,1 @@+xMNÃ0FYûs*ÿ%R @p0« RoOÚ]·ì¾·xÕesOÚD ëìØ#eáH)ö#Æá Æh1 è|VÌMbò,Ñ ËÑsÈÎú@Ö¡Xaêà)&Ùà®Úà³+Â×>Ñ7ù¿?oEt#\åtWp±ïC\Lðl;k ÝUþg\×ùúQ¶:o°Ô&Ðê¸ojÌ;³0 ó¤S-8á&ä#4VÐÀãMÞÝùß[hÀ
binary file changed (absent → 12653 bytes)
binary file changed (6426 → absent bytes)
binary file changed (absent → 18249 bytes)
binary file changed (1285 → absent bytes)
binary file changed (4566 → absent bytes)
binary file changed (2447 → absent bytes)
binary file changed (481 → absent bytes)
@@ -1,1 +0,0 @@-xN»NÄ0¤öWlu÷Ç ¡k(Añ~¬HÄ âï±BEI53Íø¼®K ò¦fÐg6ÎÓJ#irGrj¢ æÑg VÇ9ÍN"¡YJd¢éhÐÌÒºy¦èiĤqd mõ-9}[¸;àóã¸îÞn<tr8j©4ÜJ-¥ðÇ»ÊÿíNv]ÒNPx¯¥ùÚJ×B<s¹¬Ö½s·zÔ¯ù±íGñÐúÜÆv8ôð+ÿRXvHùþÄ7eÈ
@@ -1,3 +0,0 @@-x =Â0]ͯ¸± ÒIÐÅAÁB)XÁ95§òañßSDBÖ{÷½F5Àj½YÅüpðFZ·]½ó+P@-Ö{jùµç÷ÆK[2jN:¿4æ¶ËütBÉÐTd9îðB!X Ó Â VÆý÷ü©±lÇiÜvi6Ç&än-~¤l!®wD«l&öì@?Rä\(ïn9¿g` yI 4
@@ -1,6 +0,0 @@-x UÙnÛ8g}ÅAúP)µÜÊ0 à¦étÉL¾4EÙÄÐ"AQþ{ï¥$/>¾Û9wõÜØ9?ÿ:ûã¿ü>;¼¿½¿ÂÌ{{ßpµÖMPuÐÂüÝ-úVZ m=Â;£ÖznÔ¥Z2¾Ö©òvãÔ7áµ ]gùÏ$YÙ²5-WmPÞ)1þøu<kKmÇoD£e4lf#ÐCS¸ fDoiÛ:¼µ÷w¥kÄÊÅo'·öÃÃÂñ7Ép¿T^%^9ëÃósKPO¤[&̺Էf«pT8þê 8îÔ¹^'IÂyáüiaz9æj-á=tIûÄÙº²TCvnÜ{^j/[ F¸4Vþ#Ũ×l·¶¥÷]´)0* ¢ÀKu ´KUéZè²h¦k8o%ÖÈ/PÚ(¼1~N4^ú¢õZ¯ñ:G©Øàò×ÐÏ{eÌI²í&ãEâÛ¤¸(ëÀ iÆï=ó5¡FVß÷X1§=T"L ÈQDàǰwî8è!Yý©Ëð)&ó sH¸n²måÒj©¸EØv¶Ø¾&dÃr2ëÉçíº_ÐDI®ÎN4aÑRE©-¢{Üz÷©öÞÃsÓ÷ÜÉ åãFW,»ÂçÂRÕÔeEÒ]zÏî³aw}¦S2=ç´ ^;T·rýÏAø#âtC÷ ¼í Èyóø0{-OùÒüÃÛ@ÃSÇÇ¡kä§èz\º¶a{~hR½¨ °Ôº;Ýbcïû_'òÐ=è³'AÜíhÑ-iLtqÜr%IãÆ®ÚFU-v~ª%ñfþ¼Ê]/ jÌæÍµr J-HåFò5§£FI[ÙyÒ]\õwÆÒ-dç8;/ó`%x³ÎÎ^ÑÝàôiåí¶<z¤¤Î!Çt6:Ë2ºSEÆCö$¤âaZÅD?<'ýÁSJmä3ØÐÝÿoq×Ó
binary file changed (absent → 82 bytes)
binary file changed (244 → absent bytes)
binary file changed (875 → absent bytes)
binary file changed (14473 → absent bytes)
@@ -0,0 +1,2 @@+xA+Â0E]çs%iÒv".¼GÌL°HZSÁÛ[=»÷á}x\Æq¨ÐxÜÕEmZ$Ìì]Ö¦ì5%bK>'/ÌLNeéTzAÇ¡)·È~S[rÉúÈjh·²À¥Loë:ðò¯ÇyÒúdõ°Á \èt1Æö6Xkø×Xõ¿·¹^ÇêP&Xg¡ªb> ÿN©
binary file changed (absent → 298 bytes)
binary file changed (absent → 1896 bytes)
binary file changed (136 → absent bytes)
binary file changed (absent → 8803 bytes)
binary file changed (4720 → absent bytes)
binary file changed (absent → 356980 bytes)
binary file changed (4573 → absent bytes)
binary file changed (175 → absent bytes)
binary file changed (absent → 136 bytes)
binary file changed (172 → absent bytes)
binary file changed (244 → absent bytes)
@@ -1,1 +0,0 @@-xuËjÃ0E»öWd@¤U(tyñ"[?&d@ÖI&6¡ÿ^7Æ©º»{h®RÍ)¬>ÞßÖp3>cO Î5¥q>1:¯àÕOaÓc±5Î+°¯<Ú·¼¦2¬eòÈ»q9i©Hnªø_ªÁ-oY˵|ë_ÉÁÚ&Ð.Ël9ÇLÓãu¬K¶þ@@E??ÐnW´¬ÛCϪÀÓF?-~ɨÂ0îOºº_Q6ñÄÆÁ7U¦,
binary file changed (73 → absent bytes)
binary file changed (1738 → absent bytes)
binary file changed (absent → 242 bytes)
binary file changed (244 → absent bytes)
binary file changed (7687 → absent bytes)
binary file changed (54 → absent bytes)
binary file changed (208 → absent bytes)
binary file changed (absent → 1133 bytes)
binary file changed (4665 → absent bytes)
binary file changed (820 → absent bytes)
binary file changed (1669 → absent bytes)
binary file changed (absent → 6697 bytes)
binary file changed (83 → absent bytes)
binary file changed (6552 → absent bytes)
@@ -1,2 +0,0 @@-xu=Â@ó+lÜm®Ä;aS\6;aw ÿÝhÈ]"±yæTs-õl¿bÑw¬¢ã ÄÏAcM©FeOLæâ¼¶â¦ÄScØ4ŸØ6Î+ðUy´%&°h3xMeX9ÊVHErWåÄoi¨>ÕîÙxËZî¬åkGøBÖ4v9[.`Ï9f2¤×±.Ùúi\ýüDK¸^Ðb°m·ý}ÕùOä½ü4zÿö0îÓrËawð¹J´'6.îY!
@@ -1,1 +0,0 @@-xMNÃ0 Yû³ ÊØñO\q®à'M¤Æn··ÇjaÏjÞÓ÷>i(/Ë\¡Ãþ®fõÒhå$¢¡ÑX´|ÇC«QuÎ:P85Qu}g|Ï,5£ÖF*K#©8jÈE8Õ)9}x¹óñ=q])xÓÂHm¶Ê9'4®ßUþ§×÷¿ø+MNËÐ\Ê¥0Õ9'qÏÃz[PNg.kCò¦°BƼßçËv°Öb(qáϹÿ|Ôâ,nÄ»Þd
binary file changed (1292 → absent bytes)
binary file changed (209 → absent bytes)
binary file changed (8053 → absent bytes)
binary file changed (82 → absent bytes)
binary file changed (14473 → absent bytes)
binary file changed (absent → 6112 bytes)
binary file changed (absent → 2953 bytes)
binary file changed (172 → absent bytes)
@@ -1,4 +0,0 @@-xM-Â0F]çs%3iD¼+×ÓÌ´ÕoïÏÜ}Þ/OÃÐW :«B-|b£K¹a*1P鬴JEG(Ï:Vpر[¢ãÔúSüèJÑ«¶¹iÑð½¦öÓøb8Üû|µ|áqÝZo/ºú `ã R°°´ÞZ«þW-<ç¾êæ vúI
binary file changed (1666 → absent bytes)
binary file changed (8062 → absent bytes)
binary file changed (136 → absent bytes)
binary file changed (167 → absent bytes)
binary file changed (absent → 36212 bytes)
binary file changed (absent → 79 bytes)
binary file changed (82 → absent bytes)
binary file changed (absent → 107426 bytes)
binary file changed (245 → absent bytes)
binary file changed (absent → 1290 bytes)
binary file changed (absent → 73 bytes)
@@ -1,1 +0,0 @@-xË !@=S hf` 1Æì`èa?®¬Ýk¶Oï]^òÊ<n=¤C_U2{,[¡V9QB K<hõì¸ bYuê6ñÑC$òY$sµUß"å3úldë÷yµu>bÏ;ÞÏë¤ýUdÑÓO.(@Èڣβßuý·37Qí¶TéZÍCDÅ
binary file changed (absent → 106 bytes)
binary file changed (4343 → absent bytes)
binary file changed (absent → 41122 bytes)
binary file changed (6730 → absent bytes)
binary file changed (76 → absent bytes)
binary file changed (419 → absent bytes)
binary file changed (1673 → absent bytes)
binary file changed (83 → absent bytes)
binary file changed (absent → 4984 bytes)
binary file changed (4345 → absent bytes)
binary file changed (243 → absent bytes)
binary file changed (absent → 2597 bytes)
binary file changed (absent → 255 bytes)
binary file changed (absent → 8294 bytes)
binary file changed (914 → absent bytes)
binary file changed (14671 → absent bytes)
binary file changed (6468 → absent bytes)
binary file changed (absent → 1971 bytes)
binary file changed (822 → absent bytes)
binary file changed (244 → absent bytes)
binary file changed (absent → 21353 bytes)
binary file changed (absent → 482058 bytes)
binary file changed (64 → absent bytes)
binary file changed (absent → 11887 bytes)
binary file changed (absent → 245 bytes)
binary file changed (absent → 244 bytes)
binary file changed (988 → absent bytes)
binary file changed (absent → 6690 bytes)
binary file changed (215 → absent bytes)
binary file changed (55 → absent bytes)
binary file changed (absent → 664 bytes)
@@ -1,3 +0,0 @@-xK-1]ç¹ÒI:¸ðzîØ£LFÇ(x{?Gp÷ª àif½6«ÚA]ôÀ}î% <Jr!1YrìÌgÍf)îã º@ÞKð$}r,Aû-àÄ~´ó4ÛÝT_l÷¡\ìúø çm[µÝ_uõë0ºäíÀßǦÿÕæ Ç¡ÌX>Fê
binary file changed (absent → 270 bytes)
binary file changed (82 → absent bytes)
binary file changed (836 → absent bytes)
binary file changed (185 → absent bytes)
binary file changed (absent → 1244 bytes)
binary file changed (absent → 5132 bytes)
binary file changed (2151 → absent bytes)
binary file changed (2439 → absent bytes)
binary file changed (absent → 9292 bytes)
binary file changed (absent → 244 bytes)
binary file changed (246 → absent bytes)
binary file changed (absent → 4551 bytes)
binary file changed (560 → absent bytes)
binary file changed (absent → 244 bytes)
binary file changed (73 → absent bytes)
binary file changed (125 → absent bytes)
binary file changed (absent → 262 bytes)
binary file changed (8052 → absent bytes)
binary file changed (83 → absent bytes)
binary file changed (absent → 1055 bytes)
binary file changed (absent → 1129 bytes)
binary file changed (1267 → absent bytes)
binary file changed (665 → absent bytes)
binary file changed (1827 → absent bytes)
binary file changed (208 → absent bytes)
binary file changed (absent → 958 bytes)
binary file changed (208 → absent bytes)
binary file changed (208 → absent bytes)
binary file changed (1817 → absent bytes)
binary file changed (245 → absent bytes)
binary file changed (absent → 13625 bytes)
binary file changed (136 → absent bytes)
binary file changed (245 → absent bytes)
binary file changed (absent → 3615 bytes)
@@ -0,0 +1,2 @@+xÎM+Â0a×9Å\@Ió;½GdF,Ò¤ÖTðöVàîû¼¹NÓØÀXܵERd ¹mé{Ò&dJÂÆ9¨) ¼ÙR2è"mBp>híÖ¡>nþªhm·ºÀPËà²ùGþ×ã\¤=3ÍrØÆ :§½vZ«üklòVCÍë´µRkufjÂêDM\
binary file changed (absent → 23186 bytes)
binary file changed (6458 → absent bytes)
binary file changed (51 → absent bytes)
binary file changed (244 → absent bytes)
binary file changed (6496 → absent bytes)
binary file changed (absent → 4005 bytes)
binary file changed (absent → 28085 bytes)
binary file changed (absent → 356 bytes)
@@ -1,3 +0,0 @@-x½RÑjÂ0Ýs¿âB_,8Q JÙÜX7ÛØcö¶bSîߦÆMÙÛ^JrÎÍ=çÛ9ãs ïn®BØ_ûD¯ÓhzqöáeÍM «÷]1#R¢ìÃÃ-3|ª¤"Õ b^)Ü* þõç °âù!4°àl Á7í7 Í6EtUs¡N.ÀY(Ê+éù´,&ÑÛtö=FoÏ÷IeNàv4üÝ &-K.v-dëaIsZÐë >Ð<ð|¬rZ4Þ*Ü(=;ç)¨aX`R ZÃx¬¹ÑcÿmìXKúí$ìn½N-8Æhg.͵9°.JØcÑÕô¸LÄVÑtnåÚã-"¸Ì=0R}W`KiIÄ&èÖu2) CJé4DénùÔfÂyíDd®v\ý¸+aM©åN¢0̹ðc*kªØ>?4rddÔàÙl¼ätnï¸ÐaqaÑöO=çm¨÷ uY
binary file changed (82 → absent bytes)
binary file changed (absent → 7384 bytes)
binary file changed (2440 → absent bytes)
binary file changed (absent → 15403 bytes)
binary file changed (82 → absent bytes)
binary file changed (absent → 423 bytes)
@@ -1,2 +0,0 @@-xA-1E]÷¹L©·#ÖN«c¼½2ÀÕ÷~îÓT (ÒÊfU¤I·X|aÏñú89dF)§]úçÞÞ»e^÷CS{d¹éæ{ À8¤q 52¢ËËé¿;¶jU®ð¸Ä17«
binary file changed (absent → 21394 bytes)
binary file changed (absent → 172 bytes)
binary file changed (4574 → absent bytes)
binary file changed (absent → 59 bytes)
binary file changed (246 → absent bytes)
binary file changed (absent → 4488 bytes)
binary file changed (absent → 76560 bytes)
@@ -1,2 +1,2 @@ # pack-refs with: peeled fully-peeled -9bc107a80e863822b328bf64a795c3ef54e001b1 refs/remotes/origin/master+61d09a71180e5a1a4ea1d893f82a4709d8e79c27 refs/remotes/origin/master
@@ -1,1 +1,1 @@-4afd6ed7d81c474bf5c88c395a584e4f68b039ce+fd7be762cd1a7bbca6b0eeec10ab260858db340d
@@ -1,1 +1,1 @@-4afd6ed7d81c474bf5c88c395a584e4f68b039ce+40d6be12def8fc357df4248d9943bd3cc1f75f89
@@ -1,38 +1,38 @@-> {-# LANGUAGE CPP, MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}--> module Control.Arrow.ArrowP where--> import Control.Arrow-> import Control.Arrow.Operations-#if __GLASGOW_HASKELL__ >= 610-> import Control.Category-> import Prelude hiding ((.), id)-#endif--> newtype ArrowP a p b c = ArrowP { strip :: a b c }--#if __GLASGOW_HASKELL__ >= 610-> instance Category a => Category (ArrowP a p) where-> id = ArrowP id-> ArrowP g . ArrowP f = ArrowP (g . f)--> instance Arrow a => Arrow (ArrowP a p) where-> arr f = ArrowP (arr f)-> first (ArrowP f) = ArrowP (first f)-#else-> instance Arrow a => Arrow (ArrowP a p) where-> arr f = ArrowP (arr f)-> first (ArrowP f) = ArrowP (first f)-> ArrowP f >>> ArrowP g = ArrowP (f >>> g)-#endif--> instance ArrowLoop a => ArrowLoop (ArrowP a p) where-> loop (ArrowP f) = ArrowP (loop f)--> instance ArrowCircuit a => ArrowCircuit (ArrowP a p) where-> delay i = ArrowP (delay i)--> instance ArrowChoice a => ArrowChoice (ArrowP a p) where-> left (ArrowP f) = ArrowP (left f)-> ArrowP f ||| ArrowP g = ArrowP (f ||| g)-+> {-# LANGUAGE CPP, MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-} + +> module Control.Arrow.ArrowP where + +> import Control.Arrow +> import Control.Arrow.Operations +#if __GLASGOW_HASKELL__ >= 610 +> import Control.Category +> import Prelude hiding ((.), id) +#endif + +> newtype ArrowP a p b c = ArrowP { strip :: a b c } + +#if __GLASGOW_HASKELL__ >= 610 +> instance Category a => Category (ArrowP a p) where +> id = ArrowP id +> ArrowP g . ArrowP f = ArrowP (g . f) + +> instance Arrow a => Arrow (ArrowP a p) where +> arr f = ArrowP (arr f) +> first (ArrowP f) = ArrowP (first f) +#else +> instance Arrow a => Arrow (ArrowP a p) where +> arr f = ArrowP (arr f) +> first (ArrowP f) = ArrowP (first f) +> ArrowP f >>> ArrowP g = ArrowP (f >>> g) +#endif + +> instance ArrowLoop a => ArrowLoop (ArrowP a p) where +> loop (ArrowP f) = ArrowP (loop f) + +> instance ArrowCircuit a => ArrowCircuit (ArrowP a p) where +> delay i = ArrowP (delay i) + +> instance ArrowChoice a => ArrowChoice (ArrowP a p) where +> left (ArrowP f) = ArrowP (left f) +> ArrowP f ||| ArrowP g = ArrowP (f ||| g) +
@@ -1,113 +1,113 @@-> {-# LANGUAGE CPP, BangPatterns #-}--> module Control.SF.SF where--#if __GLASGOW_HASKELL__ >= 610-> import Control.Category-> import Prelude hiding ((.), id)-#endif--> import Control.Arrow-> import Control.Arrow.ArrowP-> import Control.Arrow.Operations---> newtype SF a b = SF { runSF :: (a -> (b, SF a b)) }--#if __GLASGOW_HASKELL__ >= 610-> instance Category SF where-> id = SF h where h x = (x, SF h)-> g . f = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f x-> (z, g') = runSF g y-> in f' `seq` g' `seq` (z, SF (h f' g'))--> instance Arrow SF where-> arr f = g-> where g = SF (\x -> (f x, g))-> first f = SF (g f)-> where-> g f (x, z) = f' `seq` ((y, z), SF (g f'))-> where (y, f') = runSF f x-> f &&& g = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f x-> (z, g') = runSF g x -> in ((y, z), SF (h f' g'))-> f *** g = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f (fst x)-> (z, g') = runSF g (snd x) -> in ((y, z), SF (h f' g'))-#else-> instance Arrow SF where-> arr f = g-> where g = SF (\x -> (f x, g))-> f >>> g = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f x-> (z, g') = runSF g y-> in (z, SF (h f' g'))-> first f = SF (g f)-> where-> g f (x, z) = ((y, z), SF (g f'))-> where (y, f') = runSF f x-> f &&& g = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f x-> (z, g') = runSF g x -> in ((y, z), SF (h f' g'))-> f *** g = SF (h f g)-> where-> h f g x =-> let (y, f') = runSF f (fst x)-> (z, g') = runSF g (snd x) -> in ((y, z), SF (h f' g'))-#endif--> instance ArrowLoop SF where-> loop sf = SF (g sf)-> where-> g f x = f' `seq` (y, SF (g f'))-> where ((y, z), f') = runSF f (x, z)--> instance ArrowChoice SF where-> left sf = SF (g sf)-> where -> g f x = case x of-> Left a -> let (y, f') = runSF f a in f' `seq` (Left y, SF (g f'))-> Right b -> (Right b, SF (g f))-> -> instance ArrowCircuit SF where-> delay i = SF (f i)-> where f i x = (i, SF (f x))--> run :: SF a b -> [a] -> [b]-> run _ [] = []-> run (SF f) (x:xs) =-> let (y, f') = f x -> in y `seq` f' `seq` (y : run f' xs)-> -> unfold :: SF () a -> [a]-> unfold = flip run inp-> where inp = () : inp->-> -> nth :: Int -> SF () a -> a-> nth n (SF f) = x `seq` if n == 0 then x else nth (n - 1) f'-> where (x, f') = f ()-> -> nth' :: Int -> (b, ((), b) -> (a, b)) -> a-> nth' !n (i, f) = n `seq` i `seq` f `seq` aux n i-> where-> aux !n !i = x `seq` i' `seq` if n == 0 then x else aux (n-1) i'-> where (x, i') = f ((), i)-> --+> {-# LANGUAGE CPP, BangPatterns #-} + +> module Control.SF.SF where + +#if __GLASGOW_HASKELL__ >= 610 +> import Control.Category +> import Prelude hiding ((.), id) +#endif + +> import Control.Arrow +> import Control.Arrow.ArrowP +> import Control.Arrow.Operations + + +> newtype SF a b = SF { runSF :: (a -> (b, SF a b)) } + +#if __GLASGOW_HASKELL__ >= 610 +> instance Category SF where +> id = SF h where h x = (x, SF h) +> g . f = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f x +> (z, g') = runSF g y +> in f' `seq` g' `seq` (z, SF (h f' g')) + +> instance Arrow SF where +> arr f = g +> where g = SF (\x -> (f x, g)) +> first f = SF (g f) +> where +> g f (x, z) = f' `seq` ((y, z), SF (g f')) +> where (y, f') = runSF f x +> f &&& g = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f x +> (z, g') = runSF g x +> in ((y, z), SF (h f' g')) +> f *** g = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f (fst x) +> (z, g') = runSF g (snd x) +> in ((y, z), SF (h f' g')) +#else +> instance Arrow SF where +> arr f = g +> where g = SF (\x -> (f x, g)) +> f >>> g = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f x +> (z, g') = runSF g y +> in (z, SF (h f' g')) +> first f = SF (g f) +> where +> g f (x, z) = ((y, z), SF (g f')) +> where (y, f') = runSF f x +> f &&& g = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f x +> (z, g') = runSF g x +> in ((y, z), SF (h f' g')) +> f *** g = SF (h f g) +> where +> h f g x = +> let (y, f') = runSF f (fst x) +> (z, g') = runSF g (snd x) +> in ((y, z), SF (h f' g')) +#endif + +> instance ArrowLoop SF where +> loop sf = SF (g sf) +> where +> g f x = f' `seq` (y, SF (g f')) +> where ((y, z), f') = runSF f (x, z) + +> instance ArrowChoice SF where +> left sf = SF (g sf) +> where +> g f x = case x of +> Left a -> let (y, f') = runSF f a in f' `seq` (Left y, SF (g f')) +> Right b -> (Right b, SF (g f)) +> +> instance ArrowCircuit SF where +> delay i = SF (f i) +> where f i x = (i, SF (f x)) + +> run :: SF a b -> [a] -> [b] +> run _ [] = [] +> run (SF f) (x:xs) = +> let (y, f') = f x +> in y `seq` f' `seq` (y : run f' xs) +> +> unfold :: SF () a -> [a] +> unfold = flip run inp +> where inp = () : inp +> +> +> nth :: Int -> SF () a -> a +> nth n (SF f) = x `seq` if n == 0 then x else nth (n - 1) f' +> where (x, f') = f () +> +> nth' :: Int -> (b, ((), b) -> (a, b)) -> a +> nth' !n (i, f) = n `seq` i `seq` f `seq` aux n i +> where +> aux !n !i = x `seq` i' `seq` if n == 0 then x else aux (n-1) i' +> where (x, i') = f ((), i) +> + +
@@ -1,5 +1,5 @@ name: Euterpea -version: 2.0.0 +version: 2.0.1 Cabal-Version: >= 1.8 license: BSD3 license-file: License @@ -25,12 +25,7 @@ wide-spectrum DSL, suitable for high-level music representation, algorithmic composition, and analysis; mid-level concepts such as MIDI; and low-level audio processing, sound synthesis, and instrument - design. It also includes a "musical user interface," a set of - computer-music specific GUI widgets such as keyboards, pushbuttons, - sliders, and so on. The performance of Euterpea is intended to be - as good as any other computer music language, with the goal of being - able to develop real-time applications, using both MIDI and a - high-performance back-end for real-time audio. + design. extra-source-files: ReadMe.txt, Euterpea/Examples/EnableGUI.hs @@ -70,6 +65,6 @@ HCodecs == 0.5, stm==2.4.2, containers>=0.5.5.1 && <=0.5.7.1, - bytestring>=0.10.4.0 && <= 0.10.8.0, + bytestring>=0.10.4.0 && <= 0.10.8.0, heap == 0.6.0, ghc-prim
@@ -1,19 +1,19 @@-> {-# OPTIONS -XFlexibleInstances #-}-> {-# OPTIONS -XTypeSynonymInstances #-}--> module Euterpea (-> module Euterpea.Music,-> module Euterpea.IO.Audio,-> module Euterpea.IO.MIDI,-> module Control.Arrow,-> -- This next line is from Codec.Midi-> exportFile, importFile-> ) where->-> import Euterpea.Music-> import Euterpea.IO.Audio-> import Euterpea.IO.MIDI-> import Control.Arrow-> import Codec.Midi(exportFile, importFile)-> import Control.Arrow.Operations-+> {-# OPTIONS -XFlexibleInstances #-} +> {-# OPTIONS -XTypeSynonymInstances #-} + +> module Euterpea ( +> module Euterpea.Music, +> module Euterpea.IO.Audio, +> module Euterpea.IO.MIDI, +> module Control.Arrow, +> -- This next line is from Codec.Midi +> exportFile, importFile +> ) where +> +> import Euterpea.Music +> import Euterpea.IO.Audio +> import Euterpea.IO.MIDI +> import Control.Arrow +> import Codec.Midi(exportFile, importFile) +> import Control.Arrow.Operations +
@@ -1,204 +1,204 @@-{-# LANGUAGE BangPatterns, ExistentialQuantification, - ScopedTypeVariables, FlexibleContexts #-}--module Euterpea.IO.Audio.IO (- outFile, outFileNorm, --- outFileA, outFileNormA, RecordStatus, - maxSample) where--import Control.Arrow.ArrowP-import Control.SF.SF-import Euterpea.IO.Audio.Types hiding (Signal)--import Codec.Wav-import Data.Audio-import Data.Array.Unboxed-import Data.Int----import Data.IORef---import Foreign.C---import Foreign.Marshal.Array---import Foreign.Marshal.Utils---import Foreign.Ptr---import Foreign.Storable---import Control.CCA.Types---import Control.Arrow---import Control.Concurrent.MonadIO---import Sound.RtAudio--type Signal clk a b = ArrowP SF clk a b---- | Writes sound to a wave file (.wav)-outFile :: forall a p. (AudioSample a, Clock p) => - String -- ^ Filename to write to.- -> Double -- ^ Duration of the wav in seconds.- -> Signal p () a -- ^ Signal representing the sound.- -> IO ()-outFile = outFileHelp' id--normList :: [Double] -> [Double]-normList xs = map (/ mx) xs - where mx = max 1.0 (maximum (map abs xs))---- | Like outFile, but normalizes the output if the amplitude of --- the signal goes above 1. If the maximum sample is less than--- or equal to 1, the output is not normalized.--- Currently this requires storing the entire output stream in memory--- before writing to the file.-outFileNorm :: forall a p. (AudioSample a, Clock p) => - String -- ^ Filename to write to.- -> Double -- ^ Duration of the wav in seconds.- -> Signal p () a -- ^ Signal representing the sound.- -> IO ()-outFileNorm = outFileHelp' normList--outFileHelp :: forall a p. (AudioSample a, Clock p) => - ([Double] -> [Double]) -- ^ Post-processing function.- -> String -- ^ Filename to write to.- -> Double -- ^ Duration of the wav in seconds.- -> Signal p () a -- ^ Signal representing the sound.- -> IO ()-outFileHelp f filepath dur sf = - let sr = rate (undefined :: p)- numChannels = numChans (undefined :: a)- numSamples = truncate (dur * sr) * numChannels- dat = map (fromSample . (*0.999)) - (f (toSamples dur sf)) :: [Int32]- -- multiply by 0.999 to avoid wraparound at 1.0- array = listArray (0, numSamples-1) dat- aud = Audio { sampleRate = truncate sr,- channelNumber = numChannels,- sampleData = array }- in exportFile filepath aud- -{--Alternative definition of the above that enforces a clipping behavior when -the value exceeds the [-1.0, 1.0] range. The overflow behavior makes it -very hard to debug sound modeling problems that involve certain waveforms, -such as saw waves. Clipping is also a more common behavior in other audio -software rather than overflowing or wrap-around.--}- -outFileHelp' :: forall a p. (AudioSample a, Clock p) => - ([Double] -> [Double]) -- ^ Post-processing function.- -> String -- ^ Filename to write to.- -> Double -- ^ Duration of the wav in seconds.- -> Signal p () a -- ^ Signal representing the sound.- -> IO ()-outFileHelp' f filepath dur sf = - let sr = rate (undefined :: p)- numChannels = numChans (undefined :: a)- numSamples = truncate (dur * sr) * numChannels- dat = map (fromSample . (*0.999) . clipFix) - (f (toSamples dur sf)) :: [Int32]- array = listArray (0, numSamples-1) dat- aud = Audio { sampleRate = truncate sr,- channelNumber = numChannels,- sampleData = array }- in exportFile filepath aud where- clipFix x = if x > 1.0 then 1.0 else if x < -1.0 then -1.0 else x---{--data RecordStatus = Pause | Record | Clear | Write--outFileA :: forall a. AudioSample a => - String -- ^ Filename to write to.- -> Double -- ^ Sample rate of the incoming signal.- -> UISF (a, RecordStatus) ()-outFileA = outFileHelpA id--outFileNormA :: forall a. AudioSample a => - String -- ^ Filename to write to.- -> Double -- ^ Sample rate of the incoming signal.- -> UISF (a, RecordStatus) ()-outFileNormA = outFileHelpA normList--outFileHelpA :: forall a. AudioSample a => - ([Double] -> [Double]) -- ^ Post-processing function.- -> String -- ^ Filename to write to.- -> Double -- ^ Sample rate of the incoming signal.- -> UISF (a, RecordStatus) ()-outFileHelpA f filepath sr = - let numChannels = numChans (undefined :: a)- writeWavSink = sink (writeWav f filepath sr numChannels)- in proc (a, rs) -> do- rec dat <- delay [] -< dat'- dat' <- case rs of- Pause -> returnA -< dat- Record -> returnA -< a:dat- Clear -> returnA -< []- Write -> do writeWavSink -< dat- returnA -< a:dat- returnA -< ()--}-{--writeWav :: AudioSample a => ([Double] -> [Double]) -> String -> Double -> Int -> [a] -> UI ()-writeWav f filepath sr numChannels adat = - let dat = map (fromSample . (*0.999)) - (f (concatMap collapse adat)) :: [Int32]- -- multiply by 0.999 to avoid wraparound at 1.0- array = listArray (0, (length dat)-1) dat- aud = Audio { sampleRate = truncate sr,- channelNumber = numChannels,- sampleData = array }- in liftIO $ exportFile filepath aud--}--- --toSamples :: forall a p. (AudioSample a, Clock p) =>- Double -> Signal p () a -> [Double]-toSamples dur sf = - let sr = rate (undefined :: p)- numChannels = numChans (undefined :: a)- numSamples = truncate (dur * sr) * numChannels- in take numSamples $ concatMap collapse $ unfold $ strip sf---- | Compute the maximum sample of an SF in the first 'dur' seconds.-maxSample :: forall a p. (AudioSample a, Clock p) =>- Double -> Signal p () a -> Double-maxSample dur sf = maximum (map abs (toSamples dur sf))---{--chunk !nFrames !(i, f) ref buf = nFrames `seq` i `seq` f `seq` aux nFrames i - where aux !n !i = x `seq` i `seq` i' `seq`- if n == 0 then do- writeIORef ref i- return ()- else do- pokeElemOff buf (fromIntegral nFrames-n) (realToFrac x)- aux (n-1) i'- where (x, i') = f ((), i)-{-# INLINE [0] chunk #-}--chunkify !i !f !secs = do- --userData <- new i- ref <- newIORef i- let cb :: RtAudioCallback - cb oBuf iBuf nFrames nSecs status userData = do- - lastState <- readIORef ref- -- Fill output buffer with nFrames of samples- chunk (fromIntegral nFrames) (lastState,f) ref oBuf- if secs < (realToFrac nSecs) then return 2 else return 0- - - mkAudioCallback cb ----playPure :: Show b => Double -> (b, ((), b) -> (Double, b)) -> IO ()-playPure !secs !(i, f) = do- rtaCloseStream- rtaInitialize- dev <- rtaGetDefaultOutputDevice- callback <- chunkify i f secs- with (StreamParameters dev 1 0) (\params -> do- rtaOpenStream params nullPtr float64 44100 4096 callback nullPtr nullPtr)- rtaStartStream- return ()- --}+{-# LANGUAGE BangPatterns, ExistentialQuantification, + ScopedTypeVariables, FlexibleContexts #-} + +module Euterpea.IO.Audio.IO ( + outFile, outFileNorm, +-- outFileA, outFileNormA, RecordStatus, + maxSample) where + +import Control.Arrow.ArrowP +import Control.SF.SF +import Euterpea.IO.Audio.Types hiding (Signal) + +import Codec.Wav +import Data.Audio +import Data.Array.Unboxed +import Data.Int + +--import Data.IORef +--import Foreign.C +--import Foreign.Marshal.Array +--import Foreign.Marshal.Utils +--import Foreign.Ptr +--import Foreign.Storable +--import Control.CCA.Types +--import Control.Arrow +--import Control.Concurrent.MonadIO +--import Sound.RtAudio + +type Signal clk a b = ArrowP SF clk a b + +-- | Writes sound to a wave file (.wav) +outFile :: forall a p. (AudioSample a, Clock p) => + String -- ^ Filename to write to. + -> Double -- ^ Duration of the wav in seconds. + -> Signal p () a -- ^ Signal representing the sound. + -> IO () +outFile = outFileHelp' id + +normList :: [Double] -> [Double] +normList xs = map (/ mx) xs + where mx = max 1.0 (maximum (map abs xs)) + +-- | Like outFile, but normalizes the output if the amplitude of +-- the signal goes above 1. If the maximum sample is less than +-- or equal to 1, the output is not normalized. +-- Currently this requires storing the entire output stream in memory +-- before writing to the file. +outFileNorm :: forall a p. (AudioSample a, Clock p) => + String -- ^ Filename to write to. + -> Double -- ^ Duration of the wav in seconds. + -> Signal p () a -- ^ Signal representing the sound. + -> IO () +outFileNorm = outFileHelp' normList + +outFileHelp :: forall a p. (AudioSample a, Clock p) => + ([Double] -> [Double]) -- ^ Post-processing function. + -> String -- ^ Filename to write to. + -> Double -- ^ Duration of the wav in seconds. + -> Signal p () a -- ^ Signal representing the sound. + -> IO () +outFileHelp f filepath dur sf = + let sr = rate (undefined :: p) + numChannels = numChans (undefined :: a) + numSamples = truncate (dur * sr) * numChannels + dat = map (fromSample . (*0.999)) + (f (toSamples dur sf)) :: [Int32] + -- multiply by 0.999 to avoid wraparound at 1.0 + array = listArray (0, numSamples-1) dat + aud = Audio { sampleRate = truncate sr, + channelNumber = numChannels, + sampleData = array } + in exportFile filepath aud + +{- +Alternative definition of the above that enforces a clipping behavior when +the value exceeds the [-1.0, 1.0] range. The overflow behavior makes it +very hard to debug sound modeling problems that involve certain waveforms, +such as saw waves. Clipping is also a more common behavior in other audio +software rather than overflowing or wrap-around. +-} + +outFileHelp' :: forall a p. (AudioSample a, Clock p) => + ([Double] -> [Double]) -- ^ Post-processing function. + -> String -- ^ Filename to write to. + -> Double -- ^ Duration of the wav in seconds. + -> Signal p () a -- ^ Signal representing the sound. + -> IO () +outFileHelp' f filepath dur sf = + let sr = rate (undefined :: p) + numChannels = numChans (undefined :: a) + numSamples = truncate (dur * sr) * numChannels + dat = map (fromSample . (*0.999) . clipFix) + (f (toSamples dur sf)) :: [Int32] + array = listArray (0, numSamples-1) dat + aud = Audio { sampleRate = truncate sr, + channelNumber = numChannels, + sampleData = array } + in exportFile filepath aud where + clipFix x = if x > 1.0 then 1.0 else if x < -1.0 then -1.0 else x + + +{- +data RecordStatus = Pause | Record | Clear | Write + +outFileA :: forall a. AudioSample a => + String -- ^ Filename to write to. + -> Double -- ^ Sample rate of the incoming signal. + -> UISF (a, RecordStatus) () +outFileA = outFileHelpA id + +outFileNormA :: forall a. AudioSample a => + String -- ^ Filename to write to. + -> Double -- ^ Sample rate of the incoming signal. + -> UISF (a, RecordStatus) () +outFileNormA = outFileHelpA normList + +outFileHelpA :: forall a. AudioSample a => + ([Double] -> [Double]) -- ^ Post-processing function. + -> String -- ^ Filename to write to. + -> Double -- ^ Sample rate of the incoming signal. + -> UISF (a, RecordStatus) () +outFileHelpA f filepath sr = + let numChannels = numChans (undefined :: a) + writeWavSink = sink (writeWav f filepath sr numChannels) + in proc (a, rs) -> do + rec dat <- delay [] -< dat' + dat' <- case rs of + Pause -> returnA -< dat + Record -> returnA -< a:dat + Clear -> returnA -< [] + Write -> do writeWavSink -< dat + returnA -< a:dat + returnA -< () +-} +{- +writeWav :: AudioSample a => ([Double] -> [Double]) -> String -> Double -> Int -> [a] -> UI () +writeWav f filepath sr numChannels adat = + let dat = map (fromSample . (*0.999)) + (f (concatMap collapse adat)) :: [Int32] + -- multiply by 0.999 to avoid wraparound at 1.0 + array = listArray (0, (length dat)-1) dat + aud = Audio { sampleRate = truncate sr, + channelNumber = numChannels, + sampleData = array } + in liftIO $ exportFile filepath aud +-} + + + + +toSamples :: forall a p. (AudioSample a, Clock p) => + Double -> Signal p () a -> [Double] +toSamples dur sf = + let sr = rate (undefined :: p) + numChannels = numChans (undefined :: a) + numSamples = truncate (dur * sr) * numChannels + in take numSamples $ concatMap collapse $ unfold $ strip sf + +-- | Compute the maximum sample of an SF in the first 'dur' seconds. +maxSample :: forall a p. (AudioSample a, Clock p) => + Double -> Signal p () a -> Double +maxSample dur sf = maximum (map abs (toSamples dur sf)) + + +{- +chunk !nFrames !(i, f) ref buf = nFrames `seq` i `seq` f `seq` aux nFrames i + where aux !n !i = x `seq` i `seq` i' `seq` + if n == 0 then do + writeIORef ref i + return () + else do + pokeElemOff buf (fromIntegral nFrames-n) (realToFrac x) + aux (n-1) i' + where (x, i') = f ((), i) +{-# INLINE [0] chunk #-} + +chunkify !i !f !secs = do + --userData <- new i + ref <- newIORef i + let cb :: RtAudioCallback + cb oBuf iBuf nFrames nSecs status userData = do + + lastState <- readIORef ref + -- Fill output buffer with nFrames of samples + chunk (fromIntegral nFrames) (lastState,f) ref oBuf + if secs < (realToFrac nSecs) then return 2 else return 0 + + + mkAudioCallback cb + + + +playPure :: Show b => Double -> (b, ((), b) -> (Double, b)) -> IO () +playPure !secs !(i, f) = do + rtaCloseStream + rtaInitialize + dev <- rtaGetDefaultOutputDevice + callback <- chunkify i f secs + with (StreamParameters dev 1 0) (\params -> do + rtaOpenStream params nullPtr float64 44100 4096 callback nullPtr nullPtr) + rtaStartStream + return () + +-}
@@ -1,51 +1,51 @@-{-# LANGUAGE EmptyDataDecls, FlexibleInstances #-}--module Euterpea.IO.Audio.Types where--import Control.Arrow.ArrowP-import Control.SF.SF---class Clock p where- rate :: p -> Double -- sampling rate--data AudRate-data CtrRate--instance Clock AudRate where- rate _ = 44100--instance Clock CtrRate where- rate _ = 4410--type AudSF a b = SigFun AudRate a b-type CtrSF a b = SigFun CtrRate a b--type Signal clk a b = ArrowP SF clk a b-type SigFun clk a b = ArrowP SF clk a b---- Arbitrary number of channels (say, 5.1) can be supported by just adding more--- instances of the AudioSample type class.--class AudioSample a where- zero :: a- mix :: a -> a -> a- collapse :: a -> [Double]- numChans :: a -> Int - -- allows us to reify the number of channels from the type.--instance AudioSample Double where- zero = 0- mix = (+)- collapse a = [a]- numChans _ = 1--instance AudioSample (Double,Double) where- zero = (0,0)- mix (a,b) (c,d) = (a+c,b+d)- collapse (a,b) = [a,b]- numChans _ = 2---- Some useful type synonyms:-type Mono p = Signal p () Double-type Stereo p = Signal p () (Double,Double)+{-# LANGUAGE EmptyDataDecls, FlexibleInstances #-} + +module Euterpea.IO.Audio.Types where + +import Control.Arrow.ArrowP +import Control.SF.SF + + +class Clock p where + rate :: p -> Double -- sampling rate + +data AudRate +data CtrRate + +instance Clock AudRate where + rate _ = 44100 + +instance Clock CtrRate where + rate _ = 4410 + +type AudSF a b = SigFun AudRate a b +type CtrSF a b = SigFun CtrRate a b + +type Signal clk a b = ArrowP SF clk a b +type SigFun clk a b = ArrowP SF clk a b + +-- Arbitrary number of channels (say, 5.1) can be supported by just adding more +-- instances of the AudioSample type class. + +class AudioSample a where + zero :: a + mix :: a -> a -> a + collapse :: a -> [Double] + numChans :: a -> Int + -- allows us to reify the number of channels from the type. + +instance AudioSample Double where + zero = 0 + mix = (+) + collapse a = [a] + numChans _ = 1 + +instance AudioSample (Double,Double) where + zero = (0,0) + mix (a,b) (c,d) = (a+c,b+d) + collapse (a,b) = [a,b] + numChans _ = 2 + +-- Some useful type synonyms: +type Mono p = Signal p () Double +type Stereo p = Signal p () (Double,Double)
@@ -1,282 +1,282 @@-MIDI File-writing module for use with Euterpea-Donya Quick-Last modified: 19-June-2013--This file fixes some file-writing bugs in Codec.Midi that -prevent some multi-instrument output from showing up correctly. -It defines the function exportMidiFile, which can be used like-Codec.Midi's exportFile function. Additionally, it defines two-functions for writing MIDI files, writeMidi and writeMidiA that-are like test and testA respectively but with an additional file-path argument.--NOTE #1: some of the binary handling should be redone at some -point. Currently, parts of it are using conversion to a String -type, and although it works, it should not be necessary (or at -least a cleaner way should be found).--NOTE #2: many MIDI messages are currently unsupported. The set -of supported messages is limited to those that can be produced by -Euterpea.--> module Euterpea.IO.MIDI.ExportMidiFile-> (exportMidiFile) where-> import Codec.Midi-> import Numeric-> import Data.Char-> import qualified Data.ByteString as Byte --A standard MIDI file has two main sections: a header and a -series of track chunks. Track chunks each have a track header-section and end with an end-of-track marker. Detailed infomation-on the file format can be found here:--http://faydoc.tripod.com/formats/mid.htm---> makeFile :: Midi -> Byte.ByteString-> makeFile (Midi ft td trs) = -> let ticksPerQn = -> case td of TicksPerBeat x -> x-> TicksPerSecond x y -> -> error ("(makeFile) Don't know how "++-> "to handle TicksPerSecond yet.")-> header = makeHeader ft (length trs) ticksPerQn-> body = map makeTrack trs-> in Byte.concat (header:body)--============--BUILD FILE HEADER--The standard MIDI file header starts with the following value:-4D 54 68 00 00 00 06 ff ff nn nn dd dd--ff ff is the format of the file: single-track, multi-track, or -multi-track/multi-pattern. Only the first two cases are addressed -here.--nn nn is the number of tracks in the file.--dd dd is the delta-time in ticks for a quarternote or beat.--> midiHeaderConst :: Byte.ByteString-> midiHeaderConst = -> Byte.pack [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06] --> type TrackCount = Int-> type TicksPerQN = Int---The MIDI file header is built as described above. --> makeHeader :: FileType -> TrackCount -> TicksPerQN -> Byte.ByteString-> makeHeader ft numTracks ticksPerQn = -> let -> ft' = case ft of SingleTrack -> [0x00, 0x00]-> MultiTrack -> [0x00, 0x01]-> MultiPattern -> error ("(makeHeader) Don't know "++-> "how to handle multi-pattern yet.")-> numTracks' = padByte 2 numTracks-> ticksPerQn' = padByte 2 ticksPerQn-> in if numTracks > 16 then error ("(makeHeader) Don't know how to "++-> "handle >16 tracks!")-> else Byte.concat [midiHeaderConst, Byte.pack ft', numTracks', ticksPerQn']--> padByte :: Integral a => Int -> a -> Byte.ByteString-> padByte byteCount i = -> let b = Byte.pack [fromIntegral i] -> n = Byte.length b-> padding = Byte.pack $ take (byteCount - n) $ repeat 0x00-> in if n < byteCount then Byte.concat [padding, b] else b--================--BUILDING TRACKS--A track consists of a track header, event information, and an -end-of-track marker. The track header has the format:--4D 54 72 6B xx xx xx xx--xx xx xx xx is the total number of BYTES in the track that -follows the header. This includes the end marker! This value-is obtained by generating the track first and then generating-its header.--> makeTrack :: Track Ticks -> Byte.ByteString-> makeTrack t = -> let body = makeTrackBody t-> header = makeTrackHeader body-> in Byte.concat [header, body]--> trackHeaderConst :: Byte.ByteString-> trackHeaderConst = Byte.pack [0x4D, 0x54, 0x72, 0x6B] --> makeTrackHeader :: Byte.ByteString -> Byte.ByteString-> makeTrackHeader tbody = -> let len = Byte.length tbody-> f = Byte.pack . map (fromIntegral . binStrToNum . reverse) . -> breakBinStrs 8 . pad (8*4) '0' . numToBinStr-> in Byte.concat [trackHeaderConst, f len]--Track events have two components: a variable-length delta-time and-a message. The delta-time is the number of ticks between the last -message and the next one. The format will be: time message time message ...--However, delta-times are tricky things. The fact that they can be -any length requires that they be encoded in a special way. The binary-value of the number is split into 7-bit sections. This splitting -goes from RIGHT TO LEFT (this is not in any documentation I have read,-but was the only way that worked). For n sections, the first start -with a 1 and the last starts with a 0 - thereby indicating the last -byte of the number. The following is an example of the conversion:--192 track ticks = C0 (hex) = 1100 0000 (bin) -==> converts to 8140 (hex)--Split into 7-bit groups: [1] [100 0000]-Apply padding: [000 0001] [100 0000]-Add flags: [1000 0001] [0100 0000]-Result as hex 8 1 4 0--> makeTrackBody :: Track Ticks -> Byte.ByteString -> makeTrackBody [] = endOfTrack -- end marker, very important!-> makeTrackBody ((ticks, msg):rest) = -> let b = msgToBytes msg-> b' = [to7Bits ticks, msgToBytes msg, makeTrackBody rest]-> in if Byte.length b > 0 then Byte.concat b' -> else makeTrackBody rest--The end of track marker is set 96 ticks after the last event in the -track. This offset is arbitrary, but it helps avoid clipping the notes-at the end of a file during playback in a program like Winamp or-Quicktime.--> endOfTrack = Byte.concat [to7Bits 96, Byte.pack [0xFF, 0x2F, 0x00]]--Splitting numbers into 7-bit sections and applying flags is done-by the following process:-- convert to a binary string representation-- pad the number to be full bytes-- split from right to left into groups of 7 and apply flags-- convert each 8-bit chunk back to a byte representation--> to7Bits :: (Integral a, Show a) => a -> Byte.ByteString-> to7Bits = Byte.pack . map (fromIntegral . binStrToNum . reverse) .-> fixBinStrs . map (padTo 7 . reverse). reverse . -> breakBinStrs 7 . reverse . padTo 8 . numToBinStr--Pad a binary string to be a multiple of 8 bits:--> padTo :: Int -> String -> String-> padTo i xs = if length xs `mod` i == 0 then xs else padTo i ('0':xs)--Break a string into chunks of length i:--> breakBinStrs :: Int -> String -> [String]-> breakBinStrs i s = -> if length s <= i then [s] else take i s : breakBinStrs i (drop i s)--Convert a number to a binary string:--> numToBinStr :: (Integral a, Show a) => a -> String-> numToBinStr i = showIntAtBase 2 intToDigit i ""--Convert a binary string to an integer:--> binStrToNum :: String -> Int-> binStrToNum [] = 0-> binStrToNum ('0':xs) = 2* binStrToNum xs-> binStrToNum ('1':xs) = 1 + 2*binStrToNum xs-> binStrToNum _ = error "bad data."--Append flags to a string (note, the string must be BACKWARDS):--> fixBinStrs :: [String] -> [String]-> fixBinStrs xs = -> let n = length xs-> bits = take (n-1) (repeat '1') ++ "0"-> in Prelude.zipWith (:) bits xs--Pad a list from the left until it is a fixed length:--> pad :: Int -> a -> [a] -> [a]-> pad b x xs = if length xs >= b then xs else pad b x (x:xs)--Messages have the following encodings:--8x nn vv Note Off for pitch nn at velocity vv, channel x-9x nn vv Note On for pitch nn at velocity vv, channel x-Ax nn vv Key aftertouch for pitch nn at velocity vv, channel x-Bx cc vv Control Change for controller cc with value vv, channel x-Cx pp Program Change to patch pp for channel x-Dx cc Channel after-touch to cc on channel x-Ex bb tt Pitch wheel to value ttbb, channel x (2000 hex is "normal") - (note: bb are least significant bits, tt are most significant)--Currently, only note on/off, control change, and program change are supported.--There are also META -EVENTS. This are events that have no channel number.-All meta-events have the format--FF xx nn nn dd dd ...--where xx is the command code, and nnnn is the number of bytes in the data (dd).--FF 00 nn ssss Set track sequence number-FF 01 nn tt... Text event-FF 02 nn tt... Copyright info-FF 03 nn tt... Track name-FF 04 nn tt... Track instrument name-FF 05 nn tt... Lyric-FF 06 nn tt... Marker-FF 07 nn tt... Cue point-FF 2F 00 END OF TRACK MARKER-FF 51 03 tttttt Tempo change marker, where tttttt is the microseconds per qn-FF 48 04 nnddccbb Time signature nn/dd with cc ticks per beat and bb 32nds/qn-FF 59 02 sfmi Key signature with sf sharps/flats and mi mode in {0,1}--Of these, only the end of track and tempo marker are implemented.--> msgToBytes :: Message -> Byte.ByteString-> msgToBytes (NoteOn c k v) = -> Byte.concat [Byte.pack [0x90 + fromIntegral c], padByte 1 k, padByte 1 v]-> msgToBytes (NoteOff c k v) = -> Byte.concat [Byte.pack [0x80 + fromIntegral c], padByte 1 k, padByte 1 v]-> msgToBytes (ProgramChange c p) = -> Byte.concat [Byte.pack [0xC0 + fromIntegral c], padByte 1 p]-> msgToBytes (ControlChange c n v) = -> Byte.concat [Byte.pack [0xB0 + fromIntegral c], padByte 1 n, padByte 1 v]-> msgToBytes (TempoChange t) = -- META EVENT, HAS NO CHANNEL NUMBER-> Byte.concat [Byte.pack [0xFF, 0x51, 0x03], fixTempo t]-> msgToBytes x = error ("(msgToBytes) Message type not currently "++ -> "supported: "++show x)--Fix a tempo value to be exactly 3 bytes:--> fixTempo = Byte.pack . map (fromIntegral . binStrToNum . reverse) . -> breakBinStrs 8 . pad (4*6) '0' . numToBinStr--> exportMidiFile :: FilePath -> Midi -> IO ()-> exportMidiFile fn = Byte.writeFile fn . makeFile---=================--USAGE--The exportMidiFile can now be used as follows in place of Codec.Midi's exportFile:-- writeMidi :: (ToMusic1 a) => FilePath -> Music a -> IO ()- writeMidi fn = exportMidiFile fn . testMidi-- writeMidiA :: (ToMusic1 a) => FilePath -> PMap Note1 -> Context Note1 -> Music a -> IO ()- writeMidiA fn pm con m = exportMidiFile fn $ testMidiA pm con m-- test :: (ToMusic1 a) => Music a -> IO ()- test = exportMidiFile "test.mid" . testMidi- - testA :: ToMusic1 a => PMap Note1 -> Context Note1 -> Music a -> IO ()- testA pm con m = exportMidiFile "test.mid" (testMidiA pm con m)+MIDI File-writing module for use with Euterpea +Donya Quick +Last modified: 19-June-2013 + +This file fixes some file-writing bugs in Codec.Midi that +prevent some multi-instrument output from showing up correctly. +It defines the function exportMidiFile, which can be used like +Codec.Midi's exportFile function. Additionally, it defines two +functions for writing MIDI files, writeMidi and writeMidiA that +are like test and testA respectively but with an additional file +path argument. + +NOTE #1: some of the binary handling should be redone at some +point. Currently, parts of it are using conversion to a String +type, and although it works, it should not be necessary (or at +least a cleaner way should be found). + +NOTE #2: many MIDI messages are currently unsupported. The set +of supported messages is limited to those that can be produced by +Euterpea. + +> module Euterpea.IO.MIDI.ExportMidiFile +> (exportMidiFile) where +> import Codec.Midi +> import Numeric +> import Data.Char +> import qualified Data.ByteString as Byte + +A standard MIDI file has two main sections: a header and a +series of track chunks. Track chunks each have a track header +section and end with an end-of-track marker. Detailed infomation +on the file format can be found here: + +http://faydoc.tripod.com/formats/mid.htm + + +> makeFile :: Midi -> Byte.ByteString +> makeFile (Midi ft td trs) = +> let ticksPerQn = +> case td of TicksPerBeat x -> x +> TicksPerSecond x y -> +> error ("(makeFile) Don't know how "++ +> "to handle TicksPerSecond yet.") +> header = makeHeader ft (length trs) ticksPerQn +> body = map makeTrack trs +> in Byte.concat (header:body) + +============ + +BUILD FILE HEADER + +The standard MIDI file header starts with the following value: +4D 54 68 00 00 00 06 ff ff nn nn dd dd + +ff ff is the format of the file: single-track, multi-track, or +multi-track/multi-pattern. Only the first two cases are addressed +here. + +nn nn is the number of tracks in the file. + +dd dd is the delta-time in ticks for a quarternote or beat. + +> midiHeaderConst :: Byte.ByteString +> midiHeaderConst = +> Byte.pack [0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06] + +> type TrackCount = Int +> type TicksPerQN = Int + + +The MIDI file header is built as described above. + +> makeHeader :: FileType -> TrackCount -> TicksPerQN -> Byte.ByteString +> makeHeader ft numTracks ticksPerQn = +> let +> ft' = case ft of SingleTrack -> [0x00, 0x00] +> MultiTrack -> [0x00, 0x01] +> MultiPattern -> error ("(makeHeader) Don't know "++ +> "how to handle multi-pattern yet.") +> numTracks' = padByte 2 numTracks +> ticksPerQn' = padByte 2 ticksPerQn +> in if numTracks > 16 then error ("(makeHeader) Don't know how to "++ +> "handle >16 tracks!") +> else Byte.concat [midiHeaderConst, Byte.pack ft', numTracks', ticksPerQn'] + +> padByte :: Integral a => Int -> a -> Byte.ByteString +> padByte byteCount i = +> let b = Byte.pack [fromIntegral i] +> n = Byte.length b +> padding = Byte.pack $ take (byteCount - n) $ repeat 0x00 +> in if n < byteCount then Byte.concat [padding, b] else b + +================ + +BUILDING TRACKS + +A track consists of a track header, event information, and an +end-of-track marker. The track header has the format: + +4D 54 72 6B xx xx xx xx + +xx xx xx xx is the total number of BYTES in the track that +follows the header. This includes the end marker! This value +is obtained by generating the track first and then generating +its header. + +> makeTrack :: Track Ticks -> Byte.ByteString +> makeTrack t = +> let body = makeTrackBody t +> header = makeTrackHeader body +> in Byte.concat [header, body] + +> trackHeaderConst :: Byte.ByteString +> trackHeaderConst = Byte.pack [0x4D, 0x54, 0x72, 0x6B] + +> makeTrackHeader :: Byte.ByteString -> Byte.ByteString +> makeTrackHeader tbody = +> let len = Byte.length tbody +> f = Byte.pack . map (fromIntegral . binStrToNum . reverse) . +> breakBinStrs 8 . pad (8*4) '0' . numToBinStr +> in Byte.concat [trackHeaderConst, f len] + +Track events have two components: a variable-length delta-time and +a message. The delta-time is the number of ticks between the last +message and the next one. The format will be: time message time message ... + +However, delta-times are tricky things. The fact that they can be +any length requires that they be encoded in a special way. The binary +value of the number is split into 7-bit sections. This splitting +goes from RIGHT TO LEFT (this is not in any documentation I have read, +but was the only way that worked). For n sections, the first start +with a 1 and the last starts with a 0 - thereby indicating the last +byte of the number. The following is an example of the conversion: + +192 track ticks = C0 (hex) = 1100 0000 (bin) +==> converts to 8140 (hex) + +Split into 7-bit groups: [1] [100 0000] +Apply padding: [000 0001] [100 0000] +Add flags: [1000 0001] [0100 0000] +Result as hex 8 1 4 0 + +> makeTrackBody :: Track Ticks -> Byte.ByteString +> makeTrackBody [] = endOfTrack -- end marker, very important! +> makeTrackBody ((ticks, msg):rest) = +> let b = msgToBytes msg +> b' = [to7Bits ticks, msgToBytes msg, makeTrackBody rest] +> in if Byte.length b > 0 then Byte.concat b' +> else makeTrackBody rest + +The end of track marker is set 96 ticks after the last event in the +track. This offset is arbitrary, but it helps avoid clipping the notes +at the end of a file during playback in a program like Winamp or +Quicktime. + +> endOfTrack = Byte.concat [to7Bits 96, Byte.pack [0xFF, 0x2F, 0x00]] + +Splitting numbers into 7-bit sections and applying flags is done +by the following process: +- convert to a binary string representation +- pad the number to be full bytes +- split from right to left into groups of 7 and apply flags +- convert each 8-bit chunk back to a byte representation + +> to7Bits :: (Integral a, Show a) => a -> Byte.ByteString +> to7Bits = Byte.pack . map (fromIntegral . binStrToNum . reverse) . +> fixBinStrs . map (padTo 7 . reverse). reverse . +> breakBinStrs 7 . reverse . padTo 8 . numToBinStr + +Pad a binary string to be a multiple of 8 bits: + +> padTo :: Int -> String -> String +> padTo i xs = if length xs `mod` i == 0 then xs else padTo i ('0':xs) + +Break a string into chunks of length i: + +> breakBinStrs :: Int -> String -> [String] +> breakBinStrs i s = +> if length s <= i then [s] else take i s : breakBinStrs i (drop i s) + +Convert a number to a binary string: + +> numToBinStr :: (Integral a, Show a) => a -> String +> numToBinStr i = showIntAtBase 2 intToDigit i "" + +Convert a binary string to an integer: + +> binStrToNum :: String -> Int +> binStrToNum [] = 0 +> binStrToNum ('0':xs) = 2* binStrToNum xs +> binStrToNum ('1':xs) = 1 + 2*binStrToNum xs +> binStrToNum _ = error "bad data." + +Append flags to a string (note, the string must be BACKWARDS): + +> fixBinStrs :: [String] -> [String] +> fixBinStrs xs = +> let n = length xs +> bits = take (n-1) (repeat '1') ++ "0" +> in Prelude.zipWith (:) bits xs + +Pad a list from the left until it is a fixed length: + +> pad :: Int -> a -> [a] -> [a] +> pad b x xs = if length xs >= b then xs else pad b x (x:xs) + +Messages have the following encodings: + +8x nn vv Note Off for pitch nn at velocity vv, channel x +9x nn vv Note On for pitch nn at velocity vv, channel x +Ax nn vv Key aftertouch for pitch nn at velocity vv, channel x +Bx cc vv Control Change for controller cc with value vv, channel x +Cx pp Program Change to patch pp for channel x +Dx cc Channel after-touch to cc on channel x +Ex bb tt Pitch wheel to value ttbb, channel x (2000 hex is "normal") + (note: bb are least significant bits, tt are most significant) + +Currently, only note on/off, control change, and program change are supported. + +There are also META -EVENTS. This are events that have no channel number. +All meta-events have the format + +FF xx nn nn dd dd ... + +where xx is the command code, and nnnn is the number of bytes in the data (dd). + +FF 00 nn ssss Set track sequence number +FF 01 nn tt... Text event +FF 02 nn tt... Copyright info +FF 03 nn tt... Track name +FF 04 nn tt... Track instrument name +FF 05 nn tt... Lyric +FF 06 nn tt... Marker +FF 07 nn tt... Cue point +FF 2F 00 END OF TRACK MARKER +FF 51 03 tttttt Tempo change marker, where tttttt is the microseconds per qn +FF 48 04 nnddccbb Time signature nn/dd with cc ticks per beat and bb 32nds/qn +FF 59 02 sfmi Key signature with sf sharps/flats and mi mode in {0,1} + +Of these, only the end of track and tempo marker are implemented. + +> msgToBytes :: Message -> Byte.ByteString +> msgToBytes (NoteOn c k v) = +> Byte.concat [Byte.pack [0x90 + fromIntegral c], padByte 1 k, padByte 1 v] +> msgToBytes (NoteOff c k v) = +> Byte.concat [Byte.pack [0x80 + fromIntegral c], padByte 1 k, padByte 1 v] +> msgToBytes (ProgramChange c p) = +> Byte.concat [Byte.pack [0xC0 + fromIntegral c], padByte 1 p] +> msgToBytes (ControlChange c n v) = +> Byte.concat [Byte.pack [0xB0 + fromIntegral c], padByte 1 n, padByte 1 v] +> msgToBytes (TempoChange t) = -- META EVENT, HAS NO CHANNEL NUMBER +> Byte.concat [Byte.pack [0xFF, 0x51, 0x03], fixTempo t] +> msgToBytes x = error ("(msgToBytes) Message type not currently "++ +> "supported: "++show x) + +Fix a tempo value to be exactly 3 bytes: + +> fixTempo = Byte.pack . map (fromIntegral . binStrToNum . reverse) . +> breakBinStrs 8 . pad (4*6) '0' . numToBinStr + +> exportMidiFile :: FilePath -> Midi -> IO () +> exportMidiFile fn = Byte.writeFile fn . makeFile + + +================= + +USAGE + +The exportMidiFile can now be used as follows in place of Codec.Midi's exportFile: + + writeMidi :: (ToMusic1 a) => FilePath -> Music a -> IO () + writeMidi fn = exportMidiFile fn . testMidi + + writeMidiA :: (ToMusic1 a) => FilePath -> PMap Note1 -> Context Note1 -> Music a -> IO () + writeMidiA fn pm con m = exportMidiFile fn $ testMidiA pm con m + + test :: (ToMusic1 a) => Music a -> IO () + test = exportMidiFile "test.mid" . testMidi + + testA :: ToMusic1 a => PMap Note1 -> Context Note1 -> Music a -> IO () + testA pm con m = exportMidiFile "test.mid" (testMidiA pm con m)
@@ -1,267 +1,267 @@-> module Euterpea.IO.MIDI.FromMidi (fromMidi) where-> import Euterpea.Music-> import Euterpea.IO.MIDI.ToMidi-> import Euterpea.IO.MIDI.GeneralMidi-> import Data.List-> import Codec.Midi---Donya Quick-Last updated 15-Oct-2013.--Changes since last major version (15-Jan-2013):-- makeUPM: (is !! i, 10) changed to (is !! i, 9) for Percussion.-- Instrument numbers <0 are interpreted as Percussion.-- ProgChange 10 x is now assigned (-1) as an instrument number.--KNOWN ISSUES:-- Tempo changes occuring between matching note on/off events may not be - interpreted optimally. A performance-correct representation rather - than a score-correct representation could be accomplished by looking - for these sorts of between-on-off tempo changes when calculating a - note's duration. - -This code was originally developed for research purposes and then -adapted for CPSC 431/531 to overcome some problems exhibited by the-original implementation of fromMidi. --This code has functions to read Midi values into an intermediate type,-SimpleMsg, before conversion to Music (Pitch, Volume) to make processing -instrument changes easier. The following features will be retained from -the input file:-- Placement of notes relative to the beat (assumed to be quarternotes).-- The pitch, volume, and instrument of each note.-- Tempo changes indicated by TempoChange MIDI events--Other MIDI controller information is currently not supported. This includes -events such as pitch bends and modulations. For these controllers, there is -no simple way to capture the information in a Music data structure.--The following datatype is for a simplification of MIDI events into simple -On/off events for pitches occurring at different times. There are two -types of events considered: tempo changes and note events. The note events-are represented by tuples of:-- exact onset time, Rational-- absolute pitch, AbsPitch-- volume from 0-127, Volume-- instrument number, Int. The value (-1) is used for Percussion.-- on/off type, NEvent--> data NEvent = On | Off-> deriving (Eq, Show, Ord)--> data SimpleMsg = SE (Rational, AbsPitch, Volume, Int, NEvent) |-> T (Rational, Rational)-> deriving (Eq, Show)-> instance Ord (SimpleMsg) where-> compare (SE(t,p,v,i,e)) (SE(t',p',v',i',e')) = -> if t<t' then LT else if t>t' then GT else EQ-> compare (T(t,x)) (SE(t',p',v',i',e')) = -> if t<t' then LT else if t>t' then GT else EQ-> compare (SE(t,p,v,i,e)) (T(t',x)) = -> if t<t' then LT else if t>t' then GT else EQ-> compare (T(t,x)) (T(t',x')) =-> if t<t' then LT else if t>t' then GT else EQ--The importFile function places track ticks (Ticks) in a format where -each value attached to a message represents the number of ticks that -have passed SINCE THE LAST MESSAGE. The following function will convert -input in that format into a list of pairs where the ticks are absolute. -In otherwords, ticks in the output will represent the exact point in -time of an event. This means that unsupported events (e.g. pitch bend) -can later be filtered out without affecting the timing of support events.--> addTrackTicks :: Int -> [(Ticks, a)] -> [(Ticks, a)]-> addTrackTicks sum [] = []-> addTrackTicks sum ((t,x):ts) = (t+sum,x) : addTrackTicks (t+sum) ts--The following function addresses a ticks to Music duration conversion.--> applyTD :: TimeDiv -> SimpleMsg -> SimpleMsg-> applyTD tdw x = -> case x of T(t,i) -> T(fixT tdw t, i) -> SE(t,p,v,i,e) -> SE(fixT tdw t, p, v, i, e) where--> fixT tdw t = -> case tdw of TicksPerBeat td -> t / (fromIntegral td * 4)-> TicksPerSecond fps tpf -> t / fromIntegral (fps * tpf)---The midiToEvents function will take a Midi structure (from importFile, -for example) and convert it to a list of lists of SimpleMsgs. Each outer -list represents a track in the original Midi. --> midiToEvents :: Midi -> [[SimpleMsg]]-> midiToEvents m = -> let ts = map (simplifyTrack 0) $ map (addTrackTicks 0) (tracks m) -> in distributeTempos $ map (map (applyTD $ timeDiv m)) ts where -> simplifyTrack :: Int -> [(Ticks, Message)] -> [SimpleMsg]-> simplifyTrack icur [] = []-> simplifyTrack icur ((t,m):ts) = -> case m of (NoteOn c p v) -> -> SE (fromIntegral t, p, v, icur, On) : simplifyTrack icur ts-> (NoteOff c p v) -> -> SE (fromIntegral t, p, v, icur, Off) : simplifyTrack icur ts-> (ProgramChange c p) -> simplifyTrack (if c==9 then (-1) else p) ts -> (TempoChange x) -> T (fromIntegral t, fromIntegral x) : simplifyTrack icur ts-> _ -> simplifyTrack icur ts ---The first track is the tempo track. It's events need to be distributed-across the other tracks. This function below is called for that purpose-in midiToEvents above.--> distributeTempos :: [[SimpleMsg]] -> [[SimpleMsg]]-> distributeTempos tracks = -> if length tracks > 1 then map (sort . (head tracks ++)) (tail tracks)-> else tracks -- must be a single-track file with embedded tempo changes.---The eventsToMusic function will convert a list of lists of SimpleMsgs -(output from midiToEvents) to a Music(Pitch,Volume) structure. All -notes will be connected together using the (:=:) constructor. For -example, the first line of "Frere Jaque", which would normally be-written as:--c 5 qn :+: d 5 qn :+: e 5 qn :+: c 5 qn--would actually get represented like this when read in from a MIDI:-- (rest 0 :+: c 5 qn) :=:- (rest qn :+: d 5 qn) :=:- (rest hn :+: e 5 qn) :=:- (rest dhn :+: c 5 qn)--This structure is clearly more complicated than it needs to be.-However, identifying melodic lines and phrases inorder to group the-events in a more musically appropriate manor is non-trivial, since-it requires both phrase and voice identification within an instrument -To see why this is the case, consider a Piano, which may have right -and lef thand lines that might be best separated by :=: at the -outermost level. In a MIDI, however, we are likely to get all of the-events for both hands lumped into the same track. --The parallelized structure is also required for keeping tempo changes-syced between instruments. While MIDI files allow tempo changes to -occur in the middle of a note, Euterpea's Music values do not.- -Instruments will be grouped at the outermost level. For example, if -there are 2 instruments with music values m1 and m2 repsectively, the-structure would be:-- (instrument i1 m1) :=: (instrument i2 m1)- -Tempo changes are processed within each instrument.--> eventsToMusic :: [[SimpleMsg]] -> Music (Pitch, Volume)-> eventsToMusic tracks = -> let tracks' = splitByInstruments tracks -- handle any mid-track program changes-> is = map toInstr $ map getInstrument $ filter (not.null) tracks' -- instruments-> tDef = 500000 -- current tempo, 120bpm as microseconds per qn-> in chord $ zipWith instrument is $ map (seToMusic tDef) tracks' where-> -> toInstr :: Int -> InstrumentName-> toInstr i = if i<0 then Percussion else toEnum i ->-> seToMusic :: Rational -> [SimpleMsg] -> Music (Pitch, Volume)-> seToMusic tCurr [] = rest 0-> seToMusic tCurr (e1@(SE(t,p,v,ins,On)):es) = -> let piMatch (SE(t1,p1,v1,ins1,e1)) = (p1==p && ins1==ins) && e1==Off-> piMatch (T(t1,x)) = False-> is = findIndices piMatch es -- find mactching note-offs-> SE(t1,p1,v1,ins1, e) = es !! (is !! 0) -- pick the first matching note-off-> n = (rest t :+: note (t1-t) (pitch p,v)) -- create a Music note-> in if v > 0 then -- a zero volume note is silence-> if length is > 0 then n :=: seToMusic tCurr es -- found an off-> else seToMusic tCurr ((e1:es)++[correctOff e1 es]) -- missing off case-> else seToMusic tCurr es-> seToMusic tCurr (e1@(T (t,newTempo)):es) = -> let t2 = getTime $ head es -- find time of next event after tempo change-> tfact = tCurr / newTempo -- calculate tempo change factor-> es' = map (changeTime (subtract t)) es -- adjust start times-> m = rest t :+: tempo tfact (seToMusic newTempo es') -> in if null es then rest 0 else m where-> changeTime f (SE (t,p,v,i,e)) = SE (f t,p,v,i,e)-> changeTime f (T (t,x)) = T (f t, x)-> seToMusic tCurr (_:es) = seToMusic tCurr es -- ignore note-offs (already handled)---Finding the time of an event.--> getTime (SE(t,p,v,i,e)) = t-> getTime (T (t,x)) = t---Finding the instrument associated with a track. Only the first-instrument label to appear is chosen. If a program change happens-mid-track, it will not be counted.--> getInstrument ((SE(t,p,v,i,e)):xs) = i-> getInstrument ((T x) : xs) = getInstrument xs-> getInstrument [] = -1 -- No instrument assigned---The following function ensure that only one instrument appears in -each list of SimpleMsgs. This is necessary in order to ensure that -instrument assignments occur at the outermost level of the Music.--> splitByInstruments :: [[SimpleMsg]] -> [[SimpleMsg]] -> splitByInstruments [] = []-> splitByInstruments (t:ts) = -> let i = getInstrument t-> (t',t'') = splitByI i t-> ts' = if or $ map isSE t'' then splitByInstruments (t'':ts) -> else splitByInstruments ts-> in if or $ map isSE t' then t' : ts' else ts'--> isSE :: SimpleMsg -> Bool-> isSE (SE xs) = True-> isSE (T i) = False---The splitByI function partitions a stream to select a specific instrument's events.--> splitByI :: Int -> [SimpleMsg] -> ([SimpleMsg],[SimpleMsg])-> splitByI i0 [] = ([],[])-> splitByI i0 (x:xs) = -> let (ts,fs) = splitByI i0 xs-> f (SE(_,_,_,i1,_)) = i0 == i1-> f _ = False-> in case x of SE x' -> if f x then (x:ts,fs) else (ts,x:fs)-> T i -> (x:ts, x:fs) -- add tempos to both streams---This function is an error-handling method for MIDI files which have -mismatched note on/off events. This seems to be common in output from -some software. The solution used here is to assume that the note lasts -until the the time of the last event in the list. --> correctOff (SE(t,p,v,ins,e)) [] = SE(t,p,v,ins,Off)-> correctOff (SE(t,p,v,ins,e)) es = -> let SE(t1,p1,v1,ins1,e1) = last $ filter isSE es-> in SE(t1,p,v,ins,Off) ---The fromMidi function wraps the combination of midiToEvents and -eventsToMusic and performs the final conversion to Music1.--> fromMidi :: Midi -> Music1-> fromMidi m = -> let seList = midiToEvents m-> iNums = filter (>0) $ map getInstrument seList-> upm = makeUPM $ map toEnum iNums-> in mMap (\(p,v) -> (p, [Volume v])) $ eventsToMusic seList---This function is to correct for the fact that channel 10 is-traditionally reserved for percussion. If there is no percussion,-then channel 10 must remain empty. Channels are indexed from zero -in this representation, so channel 1 is 0, channel 10 is 9, etc.--> makeUPM :: [InstrumentName] -> UserPatchMap-> makeUPM is = -> case findIndex (==Percussion) is of -> Nothing -> zip is ([0..8]++[10..]) -- no percussion-> Just i -> (is !! i, 9) : -> zip (take i is ++ drop (i+1) is) ([0..8]++[10..])-+> module Euterpea.IO.MIDI.FromMidi (fromMidi) where +> import Euterpea.Music +> import Euterpea.IO.MIDI.ToMidi +> import Euterpea.IO.MIDI.GeneralMidi +> import Data.List +> import Codec.Midi + + +Donya Quick +Last updated 15-Oct-2013. + +Changes since last major version (15-Jan-2013): +- makeUPM: (is !! i, 10) changed to (is !! i, 9) for Percussion. +- Instrument numbers <0 are interpreted as Percussion. +- ProgChange 10 x is now assigned (-1) as an instrument number. + +KNOWN ISSUES: +- Tempo changes occuring between matching note on/off events may not be + interpreted optimally. A performance-correct representation rather + than a score-correct representation could be accomplished by looking + for these sorts of between-on-off tempo changes when calculating a + note's duration. + +This code was originally developed for research purposes and then +adapted for CPSC 431/531 to overcome some problems exhibited by the +original implementation of fromMidi. + +This code has functions to read Midi values into an intermediate type, +SimpleMsg, before conversion to Music (Pitch, Volume) to make processing +instrument changes easier. The following features will be retained from +the input file: +- Placement of notes relative to the beat (assumed to be quarternotes). +- The pitch, volume, and instrument of each note. +- Tempo changes indicated by TempoChange MIDI events + +Other MIDI controller information is currently not supported. This includes +events such as pitch bends and modulations. For these controllers, there is +no simple way to capture the information in a Music data structure. + +The following datatype is for a simplification of MIDI events into simple +On/off events for pitches occurring at different times. There are two +types of events considered: tempo changes and note events. The note events +are represented by tuples of: +- exact onset time, Rational +- absolute pitch, AbsPitch +- volume from 0-127, Volume +- instrument number, Int. The value (-1) is used for Percussion. +- on/off type, NEvent + +> data NEvent = On | Off +> deriving (Eq, Show, Ord) + +> data SimpleMsg = SE (Rational, AbsPitch, Volume, Int, NEvent) | +> T (Rational, Rational) +> deriving (Eq, Show) +> instance Ord (SimpleMsg) where +> compare (SE(t,p,v,i,e)) (SE(t',p',v',i',e')) = +> if t<t' then LT else if t>t' then GT else EQ +> compare (T(t,x)) (SE(t',p',v',i',e')) = +> if t<t' then LT else if t>t' then GT else EQ +> compare (SE(t,p,v,i,e)) (T(t',x)) = +> if t<t' then LT else if t>t' then GT else EQ +> compare (T(t,x)) (T(t',x')) = +> if t<t' then LT else if t>t' then GT else EQ + +The importFile function places track ticks (Ticks) in a format where +each value attached to a message represents the number of ticks that +have passed SINCE THE LAST MESSAGE. The following function will convert +input in that format into a list of pairs where the ticks are absolute. +In otherwords, ticks in the output will represent the exact point in +time of an event. This means that unsupported events (e.g. pitch bend) +can later be filtered out without affecting the timing of support events. + +> addTrackTicks :: Int -> [(Ticks, a)] -> [(Ticks, a)] +> addTrackTicks sum [] = [] +> addTrackTicks sum ((t,x):ts) = (t+sum,x) : addTrackTicks (t+sum) ts + +The following function addresses a ticks to Music duration conversion. + +> applyTD :: TimeDiv -> SimpleMsg -> SimpleMsg +> applyTD tdw x = +> case x of T(t,i) -> T(fixT tdw t, i) +> SE(t,p,v,i,e) -> SE(fixT tdw t, p, v, i, e) where + +> fixT tdw t = +> case tdw of TicksPerBeat td -> t / (fromIntegral td * 4) +> TicksPerSecond fps tpf -> t / fromIntegral (fps * tpf) + + +The midiToEvents function will take a Midi structure (from importFile, +for example) and convert it to a list of lists of SimpleMsgs. Each outer +list represents a track in the original Midi. + +> midiToEvents :: Midi -> [[SimpleMsg]] +> midiToEvents m = +> let ts = map (simplifyTrack 0) $ map (addTrackTicks 0) (tracks m) +> in distributeTempos $ map (map (applyTD $ timeDiv m)) ts where +> simplifyTrack :: Int -> [(Ticks, Message)] -> [SimpleMsg] +> simplifyTrack icur [] = [] +> simplifyTrack icur ((t,m):ts) = +> case m of (NoteOn c p v) -> +> SE (fromIntegral t, p, v, icur, On) : simplifyTrack icur ts +> (NoteOff c p v) -> +> SE (fromIntegral t, p, v, icur, Off) : simplifyTrack icur ts +> (ProgramChange c p) -> simplifyTrack (if c==9 then (-1) else p) ts +> (TempoChange x) -> T (fromIntegral t, fromIntegral x) : simplifyTrack icur ts +> _ -> simplifyTrack icur ts + + +The first track is the tempo track. It's events need to be distributed +across the other tracks. This function below is called for that purpose +in midiToEvents above. + +> distributeTempos :: [[SimpleMsg]] -> [[SimpleMsg]] +> distributeTempos tracks = +> if length tracks > 1 then map (sort . (head tracks ++)) (tail tracks) +> else tracks -- must be a single-track file with embedded tempo changes. + + +The eventsToMusic function will convert a list of lists of SimpleMsgs +(output from midiToEvents) to a Music(Pitch,Volume) structure. All +notes will be connected together using the (:=:) constructor. For +example, the first line of "Frere Jaque", which would normally be +written as: + +c 5 qn :+: d 5 qn :+: e 5 qn :+: c 5 qn + +would actually get represented like this when read in from a MIDI: + + (rest 0 :+: c 5 qn) :=: + (rest qn :+: d 5 qn) :=: + (rest hn :+: e 5 qn) :=: + (rest dhn :+: c 5 qn) + +This structure is clearly more complicated than it needs to be. +However, identifying melodic lines and phrases inorder to group the +events in a more musically appropriate manor is non-trivial, since +it requires both phrase and voice identification within an instrument +To see why this is the case, consider a Piano, which may have right +and lef thand lines that might be best separated by :=: at the +outermost level. In a MIDI, however, we are likely to get all of the +events for both hands lumped into the same track. + +The parallelized structure is also required for keeping tempo changes +syced between instruments. While MIDI files allow tempo changes to +occur in the middle of a note, Euterpea's Music values do not. + +Instruments will be grouped at the outermost level. For example, if +there are 2 instruments with music values m1 and m2 repsectively, the +structure would be: + + (instrument i1 m1) :=: (instrument i2 m1) + +Tempo changes are processed within each instrument. + +> eventsToMusic :: [[SimpleMsg]] -> Music (Pitch, Volume) +> eventsToMusic tracks = +> let tracks' = splitByInstruments tracks -- handle any mid-track program changes +> is = map toInstr $ map getInstrument $ filter (not.null) tracks' -- instruments +> tDef = 500000 -- current tempo, 120bpm as microseconds per qn +> in chord $ zipWith instrument is $ map (seToMusic tDef) tracks' where +> +> toInstr :: Int -> InstrumentName +> toInstr i = if i<0 then Percussion else toEnum i +> +> seToMusic :: Rational -> [SimpleMsg] -> Music (Pitch, Volume) +> seToMusic tCurr [] = rest 0 +> seToMusic tCurr (e1@(SE(t,p,v,ins,On)):es) = +> let piMatch (SE(t1,p1,v1,ins1,e1)) = (p1==p && ins1==ins) && e1==Off +> piMatch (T(t1,x)) = False +> is = findIndices piMatch es -- find mactching note-offs +> SE(t1,p1,v1,ins1, e) = es !! (is !! 0) -- pick the first matching note-off +> n = (rest t :+: note (t1-t) (pitch p,v)) -- create a Music note +> in if v > 0 then -- a zero volume note is silence +> if length is > 0 then n :=: seToMusic tCurr es -- found an off +> else seToMusic tCurr ((e1:es)++[correctOff e1 es]) -- missing off case +> else seToMusic tCurr es +> seToMusic tCurr (e1@(T (t,newTempo)):es) = +> let t2 = getTime $ head es -- find time of next event after tempo change +> tfact = tCurr / newTempo -- calculate tempo change factor +> es' = map (changeTime (subtract t)) es -- adjust start times +> m = rest t :+: tempo tfact (seToMusic newTempo es') +> in if null es then rest 0 else m where +> changeTime f (SE (t,p,v,i,e)) = SE (f t,p,v,i,e) +> changeTime f (T (t,x)) = T (f t, x) +> seToMusic tCurr (_:es) = seToMusic tCurr es -- ignore note-offs (already handled) + + +Finding the time of an event. + +> getTime (SE(t,p,v,i,e)) = t +> getTime (T (t,x)) = t + + +Finding the instrument associated with a track. Only the first +instrument label to appear is chosen. If a program change happens +mid-track, it will not be counted. + +> getInstrument ((SE(t,p,v,i,e)):xs) = i +> getInstrument ((T x) : xs) = getInstrument xs +> getInstrument [] = -1 -- No instrument assigned + + +The following function ensure that only one instrument appears in +each list of SimpleMsgs. This is necessary in order to ensure that +instrument assignments occur at the outermost level of the Music. + +> splitByInstruments :: [[SimpleMsg]] -> [[SimpleMsg]] +> splitByInstruments [] = [] +> splitByInstruments (t:ts) = +> let i = getInstrument t +> (t',t'') = splitByI i t +> ts' = if or $ map isSE t'' then splitByInstruments (t'':ts) +> else splitByInstruments ts +> in if or $ map isSE t' then t' : ts' else ts' + +> isSE :: SimpleMsg -> Bool +> isSE (SE xs) = True +> isSE (T i) = False + + +The splitByI function partitions a stream to select a specific instrument's events. + +> splitByI :: Int -> [SimpleMsg] -> ([SimpleMsg],[SimpleMsg]) +> splitByI i0 [] = ([],[]) +> splitByI i0 (x:xs) = +> let (ts,fs) = splitByI i0 xs +> f (SE(_,_,_,i1,_)) = i0 == i1 +> f _ = False +> in case x of SE x' -> if f x then (x:ts,fs) else (ts,x:fs) +> T i -> (x:ts, x:fs) -- add tempos to both streams + + +This function is an error-handling method for MIDI files which have +mismatched note on/off events. This seems to be common in output from +some software. The solution used here is to assume that the note lasts +until the the time of the last event in the list. + +> correctOff (SE(t,p,v,ins,e)) [] = SE(t,p,v,ins,Off) +> correctOff (SE(t,p,v,ins,e)) es = +> let SE(t1,p1,v1,ins1,e1) = last $ filter isSE es +> in SE(t1,p,v,ins,Off) + + +The fromMidi function wraps the combination of midiToEvents and +eventsToMusic and performs the final conversion to Music1. + +> fromMidi :: Midi -> Music1 +> fromMidi m = +> let seList = midiToEvents m +> iNums = filter (>0) $ map getInstrument seList +> upm = makeUPM $ map toEnum iNums +> in mMap (\(p,v) -> (p, [Volume v])) $ eventsToMusic seList + + +This function is to correct for the fact that channel 10 is +traditionally reserved for percussion. If there is no percussion, +then channel 10 must remain empty. Channels are indexed from zero +in this representation, so channel 1 is 0, channel 10 is 9, etc. + +> makeUPM :: [InstrumentName] -> UserPatchMap +> makeUPM is = +> case findIndex (==Percussion) is of +> Nothing -> zip is ([0..8]++[10..]) -- no percussion +> Just i -> (is !! i, 9) : +> zip (take i is ++ drop (i+1) is) ([0..8]++[10..]) +
@@ -43,13 +43,15 @@ > -- timing musicToMEventss > metro :: Int -> Dur -> DurT > metro setting dur = 60 / (fromIntegral setting * dur) -> applyControls :: Music1 -> Music1 -> applyControls (Modify (Tempo r) m) = scaleDurations r $ applyControls m -> applyControls (Modify (Transpose k) m) = shiftPitches1 k $ applyControls m -> applyControls (m1 :+: m2) = applyControls m1 :+: applyControls m2 -> applyControls (m1 :=: m2) = applyControls m1 :=: applyControls m2 -> applyControls x = x +> applyControls :: Music1 -> Music1 +> applyControls (Modify (Tempo r) m) = scaleDurations r $ applyControls m +> applyControls (Modify (Transpose k) m) = shiftPitches1 k $ applyControls m +> applyControls (Modify x m) = Modify x $ applyControls m +> applyControls (m1 :+: m2) = applyControls m1 :+: applyControls m2 +> applyControls (m1 :=: m2) = applyControls m1 :=: applyControls m2 +> applyControls x = x + > musicToMEvents :: MContext -> Music1 -> (Performance, DurT) > musicToMEvents c@MContext{mcTime=t, mcDur=dt} (Prim (Note d p)) = ([noteToMEvent c d p], d*dt) > musicToMEvents c@MContext{mcTime=t, mcDur=dt} (Prim (Rest d)) = ([], d*dt) @@ -63,7 +65,9 @@ > in (merge evs1 evs2, max d1 d2) > musicToMEvents c (Modify (Instrument i) m) = musicToMEvents c{mcInst=i} m > musicToMEvents c (Modify (Phrase pas) m) = phraseToMEvents c pas m -> musicToMEvents c (Modify x m) = musicToMEvents c m -- Transpose and Tempo addressed by applyControls +> musicToMEvents c (Modify (KeySig x y) m) = musicToMEvents c m -- KeySig causes no change +> musicToMEvents c (Modify (Custom x) m) = musicToMEvents c m -- Custom cuases no change +> musicToMEvents c m@(Modify x m') = musicToMEvents c $ applyControls m -- Transpose and Tempo addressed by applyControls > noteToMEvent :: MContext -> Dur -> (Pitch, [NoteAttribute]) -> MEvent > noteToMEvent c@(MContext ct ci cdur cvol) d (p, nas) =
@@ -1,665 +1,665 @@--> {-# LANGUAGE GeneralizedNewtypeDeriving #-}-> module Euterpea.IO.MIDI.MidiIO (-> getAllDevices, --isValidInputDevice, isValidOutputDevice, -- Used only by Euterpea.IO.MUI.MidiWidgets-> terminateMidi, initializeMidi, -- Used only by Euterpea.IO.MUI-> outputMidi, deliverMidiEvent, -- Used only by Euterpea.IO.MUI.MidiWidgets (particularly by midiOut)-> pollMidi, -- Used only by Euterpea.IO.MUI.MidiWidgets (particularly by midiIn)-> defaultOutput, defaultInput,-> playMidi, -> MidiMessage (ANote, Std), -> getTimeNow,-> DeviceInfo(..), InputDeviceID, OutputDeviceID, Message(..), Time,-> unsafeInputID, unsafeOutputID,-> ) where--> import Codec.Midi (Time, Channel, Key, Velocity, -> Message (..), Midi (..), Track, -> toRealTime, toAbsTime, toSingleTrack, isTrackEnd)-> import Sound.PortMidi (DeviceInfo (..), getDeviceInfo, -> DeviceID, countDevices, time, -> getDefaultOutputDeviceID, getDefaultInputDeviceID, -> openInput, openOutput, readEvents, -> close, writeShort, getErrorText, terminate, initialize, -> PMError (NoError, BufferOverflow), PMStream, -> PMEvent (..), PMMsg (PMMsg))-> import Control.Exception (finally)-> import Control.Concurrent-> import Control.Concurrent.STM.TChan-> import Control.Monad.STM (atomically)-> import Data.IORef--> import Data.Bits (shiftR, shiftL, (.|.), (.&.))-> import Data.List (findIndex)-> import Data.Maybe (mapMaybe)-> import qualified Data.Heap as Heap--> import System.IO (hPutStrLn, stderr)-> import System.IO.Unsafe (unsafePerformIO)-> import Control.DeepSeq (NFData)-------------------------------- | Midi Type declarations | -------------------------------> type MidiEvent = (Time, MidiMessage)--> data MidiMessage = ANote { channel :: !Channel, key :: !Key,-> velocity :: !Velocity, duration :: !Time }-> | Std Message-> deriving Show--> newtype InputDeviceID = InputDeviceID DeviceID-> deriving (Eq, Show, NFData)-> newtype OutputDeviceID = OutputDeviceID DeviceID-> deriving (Eq, Show, NFData)--> unsafeInputID :: Int -> InputDeviceID-> unsafeInputID = InputDeviceID--> unsafeOutputID :: Int -> OutputDeviceID-> unsafeOutputID = OutputDeviceID------------- | Time | -------------Is this the time we want? This comes from PortMidi, but there's also the -function FRP.UISF.SOE.timeGetTime which uses time data from GLFW.--> getTimeNow :: IO Time -> getTimeNow = do-> t <- time-> return (fromIntegral t / 1000)-------------------------- | Device Functions | -------------------------getAllDevices returns a list of all of the DeviceInfos found.-It calls Port.Midi.getDeviceInfo over all device numbers--> getAllDevices :: IO ([(InputDeviceID, DeviceInfo)], [(OutputDeviceID, DeviceInfo)])-> getAllDevices = do-> n <- countDevices-> deviceInfos <- mapM getDeviceInfo [0..n-1]-> let devs = zip [0..n-1] deviceInfos-> return ([ (InputDeviceID d, i) | (d,i) <- devs, input i], -> [ (OutputDeviceID d, i) | (d,i) <- devs, output i])---isValidInputDevice and isValideOutputDevice check whether the given -devices are respectively valid for input or output.--isValidInputDevice, isValidOutputDevice :: DeviceID -> IO Bool-isValidInputDevice = isValidDevice input-isValidOutputDevice = isValidDevice output-isValidDevice :: (DeviceInfo -> Bool) -> DeviceID -> IO Bool-isValidDevice pred i = do- n <- countDevices - info <- getAllDevices- return $ - i >= 0 && i < n && pred (snd $ info !! i)------------------------- | Default devices | ------------------------Rather than export the deviceIDs directly, these two functions allow -the caller to use the DeviceID without directly controlling it.--They take a function (such as playMidi) and an auxiary argument and -apply them together with the default device. If no default device -exists, an error is thrown.--> defaultOutput :: (OutputDeviceID -> a -> IO b) -> a -> IO b-> defaultOutput f a = do-> i <- getDefaultOutputDeviceID-> case i of-> Nothing -> error "No MIDI output device found"-> Just i -> f (OutputDeviceID i) a-> -> defaultInput :: (InputDeviceID -> a -> IO b) -> a -> IO b-> defaultInput f a = do-> i <- getDefaultInputDeviceID-> case i of-> Nothing -> error "No MIDI input device found"-> Just i -> f (InputDeviceID i) a--------------------------- | Priority Channels | --------------------------The priority channel data type and a constructor for it will be used -by devices. We define them here.--> data PrioChannel a b = PrioChannel-> { get :: IO (Heap.MinPrioHeap a b),-> push :: a -> b -> IO (),-> pop :: IO (a,b),-> peek :: IO (Maybe (a,b)) }--> makePriorityChannel :: IO (PrioChannel Time Message)-> makePriorityChannel = do-> heapRef <- newIORef (Heap.empty :: Heap.MinPrioHeap Time Message)-> let get = readIORef heapRef-> push a b = modifyIORef heapRef (Heap.insert (a,b))-> pop = do-> h <- get-> let (a, h') = Heap.extractHead h-> modifyIORef heapRef (\_ -> h')-> return a-> peek = do-> h <- get-> if Heap.isEmpty h -> then return Nothing -> else return $ Just $ Heap.head h-> -> return $ PrioChannel get push pop peek---------------------------- | Global Device Data | ---------------------------We keep a mapping from DeviceID to the priority channel for keeping-track of future MIDI messages, an output function to produce sound, -and a stop function. This mapping is stored in the global ref -outDevMap, and it is accessed by getOutDev (which looks up info -and adds associations if necessary) and terminateMidi (which calls -the stop function on all elements and clears the mapping).--outDevMap is the global mapping.--> outDevMap :: IORef [(OutputDeviceID, -> (PrioChannel Time Message, -- priority channel-> (Time, Message) -> IO (), -- sound output function-> IO ()))] -- stop/terminate function-> outDevMap = unsafePerformIO $ newIORef []---outPort and inPort are global memory refs that contain a mapping of -DeviceID to Port Midi Streams. They are modified with addPort (which -adds a new mapping to the list) and lookupPort (which, given a DeviceID, -returns the Port Midi Stream associated with it).--> outPort :: IORef [(OutputDeviceID, PMStream)]-> inPort :: IORef [(InputDeviceID, PMStream)]-> outPort = unsafePerformIO (newIORef [])-> inPort = unsafePerformIO (newIORef [])--> lookupPort :: (Eq deviceid) => IORef [(deviceid, PMStream)] -> deviceid -> IO (Maybe PMStream)-> lookupPort p i = readIORef p >>= (return . lookup i)--> addPort :: IORef [(deviceid, PMStream)] -> (deviceid, PMStream) -> IO ()-> addPort p is = modifyIORef p (is:)------------------------------------------------------ | Global Device Initialization and Termination | -----------------------------------------------------initializeMidi just initializes PortMidi--> initializeMidi :: IO ()-> initializeMidi = do-> e <- initialize-> if e == NoError -> then return () -> else reportError "initializeMidi" e--terminateMidi calls the stop function on all elements of outDevMap -and clears the mapping entirely. It also clears outPort and inPort.--> terminateMidi :: IO ()-> terminateMidi = do-> inits <- readIORef outDevMap-> mapM_ (\(_, (_,_out,stop)) -> stop) inits-> terminate-> modifyIORef outDevMap (const [])-> writeIORef outPort []-> writeIORef inPort []----------------------- | Device Lookup | ----------------------getOutDev looks up info in outDevMap and adds associations if necessary. -It is accessed as a helper function for outputMidi and deliverMidiEvent.--> getOutDev :: OutputDeviceID -> IO (PrioChannel Time Message, (Time, Message) -> IO (), IO ())-> getOutDev devId = do-> inits <- readIORef outDevMap-> case lookup devId inits of-> Just f -> return f-> Nothing -> do-> x <- midiOutRealTime' devId -- Changes made by Donya Quick: this line used to pattern match against Just.-> pChan <- makePriorityChannel-> case x of Just (mout,stop) -> do -- Case statement added.-> modifyIORef outDevMap ((devId,(pChan,mout,stop)):)-> return (pChan,mout,stop)-> Nothing -> return (pChan, const (return ()), return ()) -- Nothing case added-------------------- | Midi Input | -------------------pollMidi take an input device and a callback function and polls the device -for midi events. Any events are sent, along with the current time, to -the callback function.-DWC NOTE: Why is the time even used? All messages get the same time?--> pollMidiCB :: InputDeviceID -> ((Time, [Message]) -> IO ()) -> IO ()-> pollMidiCB idid@(InputDeviceID devId) callback = do-> s <- lookupPort inPort idid -> case s of-> Nothing -> do-> r <- openInput devId -> case r of-> Right e -> reportError "pollMidiCB" e-> Left s -> addPort inPort (idid, s) >> input s-> Just s -> input s -> where-> input :: PMStream -> IO ()-> input s = do-> e <- readEvents s-> case e of-> Right e -> if e == NoError -> then return () -> else reportError "pollMidiCB" e-> Left l -> do-> now <- getTimeNow-> case mapMaybe (msgToMidi . message) l of-> [] -> return ()-> ms -> callback (now, ms)--> pollMidi :: InputDeviceID -> IO (Maybe (Time, [Message]))-> pollMidi idid@(InputDeviceID devId) = do-> s <- lookupPort inPort idid -> case s of-> Nothing -> do-> r <- openInput devId -> case r of-> Right e -> reportError "pollMIDI" e >> return Nothing-> Left s -> addPort inPort (idid, s) >> input s-> Just s -> input s -> where-> input :: PMStream -> IO (Maybe (Time, [Message]))-> input s = do-> e <- readEvents s-> case e of-> Right e -> if e == NoError -> then return Nothing-> else reportError "pollMIDI" e >> return Nothing-> Left l -> do-> now <- getTimeNow-> case mapMaybe (msgToMidi . message) l of-> [] -> return Nothing-> ms -> return $ Just (now, ms)------------------------------------------------- | Midi Output for inidividual Midi events | ------------------------------------------------The following two functions are for sending and playing individual -Midi events to devices. Typically, usage will be to call outputMidi -to play anything that's ready to play and then send in the latest -messages with deliverMidiEvent. Of course, if no new messages are -ready to be delivered, that step can be omitted. Either way, -outputMidi should be called many times per second to assure that -all Midi messages are played approximately when scheduled.--deliverMidiEvent sends the given MidiEvent to the given device. If -the event is scheduled to happen ``now'', then it is immediately -played. Otherwise, it is queued for later.--> deliverMidiEvent :: OutputDeviceID -> MidiEvent -> IO ()-> deliverMidiEvent devId (t,m) = do-> (pChan, out, _stop) <- getOutDev devId-> now <- getTimeNow-> let deliver t m = do-> if t == 0-> then out (now,m) -> else push pChan (now+t) m-> -> case m of-> Std m -> deliver t m-> ANote c k v d -> do-> deliver t (NoteOn c k v)-> deliver (t+d) (NoteOff c k v)---outputMidi plays all midi events that are waiting in this device's -priority queue whose time to play has come.--> outputMidi :: OutputDeviceID -> IO ()-> outputMidi devId = do-> (pChan, out, _stop) <- getOutDev devId-> let loop = do-> r <- peek pChan-> case r of-> Nothing -> return ()-> Just (t,m) -> do-> now <- getTimeNow-> if t <= now -> then out (now, m) >> pop pChan >> loop-> else return ()-> loop-> return ()----------------------------------------------- | Midi Output for a complete Midi track | ----------------------------------------------When an entire Midi track is ready to be played, the playMidi function -may be more appropriate than deliverMidiEvent and outputMidi.--playMidi will queue up the entire Midi track given to it and then close -the output device.--> playMidi :: OutputDeviceID -> Midi -> IO ()-> playMidi device midi@(Midi _ division _) = do-> let track = toRealTime division (toAbsTime (head (tracks (toSingleTrack midi))))-> out <- midiOutRealTime device-> case out of-> Nothing -> return ()-> Just (out, stop) -> do-> t0 <- getTimeNow -> finally (playTrack t0 0 out track) stop-> where-> playTrack t0 t' out [] = out (t0 + t', TrackEnd)-> playTrack t0 t' out (e@(t, m) : s) = do-> out (t0 + t, m) -> if isTrackEnd m -> then return ()-> else playTrack t0 t out s------------------------- | midiOutRealTime | ------------------------The following two functions are used to open a device for Midi output. -They should only be called when the device hasn't yet been opened, and -they both return a ``play'' function and a ``stop'' function.--Currently, midiOutRealTime' is used for Midi output for inidividual -Midi events, and midiOutRealTime is used for Midi output for a complete -Midi track.--DWC Notes:-I'm not entirely sure how they both work yet. midiOutRealTime' -actually looks pretty straightforward in that it just creates the process -and stop functions and adds this device to the outPort device list. The -process function will look up the device in the outPort device list, and -if it finds it, it writes the message to it. The stop function removes -the device from the outPort list and closes it.--On the other hand, midiOutRealTime spawns a new thread and does some -concurrent stuff. Really, it looks similar, but I don't know when to -use one and when to use the other.--> midiOutRealTime' :: OutputDeviceID -> IO (Maybe ((Time, Message) -> IO (), IO ()))-> midiOutRealTime' odid@(OutputDeviceID devId) = do-> s <- openOutput devId 1 -> case s of-> Right e -> reportError "Unable to open output device in midiOutRealTime'" e >> return Nothing-> Left s -> do-> addPort outPort (odid, s)-> return $ Just (process odid, finalize odid)-> where-> process odid (t, msg) = do-> s <- lookupPort outPort odid-> case s of-> Nothing -> error ("midiOutRealTime': port " ++ show odid ++ " is not open for output")-> Just s -> do-> if isTrackEnd msg -> then return ()-> else case midiEvent msg of-> Just m -> writeMsg s t m-> Nothing -> return ()-> writeMsg s t m = do-> e <- writeShort s (PMEvent m (round (t * 1e3)))-> case e of-> NoError -> return () -> _ -> reportError "midiOutRealTime'" e-> finalize odid = do-> s <- lookupPort outPort odid-> e <- maybe (return NoError) close s-> case e of-> NoError -> return () -> _ -> reportError "midiOutRealTime'" e---> midiOutRealTime :: OutputDeviceID -> IO (Maybe ((Time, Message) -> IO (), IO ()))-> midiOutRealTime odid@(OutputDeviceID devId) = do-> s <- openOutput devId 1 -> case s of-> Right e -> reportError "outputMidi" e >> return Nothing-> Left s -> do-> ch <- atomically newTChan -> wait <- newEmptyMVar-> fin <- newEmptyMVar-> forkIO (pump s ch wait fin)-> return $ Just (output s ch wait, stop ch fin)-> where-> stop ch fin = atomically (unGetTChan ch Nothing) >> takeMVar fin-> output s ch wait evt@(_, m) = do-> atomically $ writeTChan ch (Just evt)-> if isTrackEnd m then takeMVar wait else return ()-> pump s ch wait fin = loop-> where-> loop = do -> e <- atomically $ readTChan ch-> case e of-> Nothing -> close s >> putMVar fin ()-> Just (t, msg) -> do-> now <- getTimeNow-> if (t > now + 5) -> then atomically (unGetTChan ch e) >> threadDelay 10000 >> loop-> else do -> done <- process t msg-> if done -> then waitUntil (t + 1)-> else loop -> where-> waitUntil t = do-> now <- getTimeNow-> if t > now -> then do-> threadDelay $ min 10000 (round((t - now) * 1E6)) -> empty <- atomically $ isEmptyTChan ch-> if empty -> then waitUntil t-> else do-> e <- atomically $ readTChan ch-> case e of-> Nothing -> finishup -> _ -> waitUntil t-> else finishup-> finishup = putMVar wait () >> close s >> putMVar fin ()-> process t msg = if isTrackEnd msg -> then return True -> else case midiEvent msg of-> Just m -> writeMsg t m-> Nothing -> return False -> writeMsg t m = do-> e <- writeShort s (PMEvent m (round (t * 1e3)))-> case e of-> NoError -> return False -> BufferOverflow -> putStrLn "overflow" >> threadDelay 10000 >> writeMsg t m-> _ -> reportError "outputMidi" e >> return True ------------------------- | MIDI Conversion | ------------------------A conversion function from Codec.Midi Messages to PortMidi PMMsgs.--> midiEvent :: Message -> Maybe PMMsg-> midiEvent (NoteOff c p v) = Just $ PMMsg (128 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral v)-> midiEvent (NoteOn c p v) = Just $ PMMsg (144 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral v)-> midiEvent (KeyPressure c p pr) = Just $ PMMsg (160 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral pr)-> midiEvent (ControlChange c cn cv) = Just $ PMMsg (176 .|. (fromIntegral c .&. 0xF)) (fromIntegral cn) (fromIntegral cv)-> midiEvent (ProgramChange c pn) = Just $ PMMsg (192 .|. (fromIntegral c .&. 0xF)) (fromIntegral pn) 0-> midiEvent (ChannelPressure c pr) = Just $ PMMsg (208 .|. (fromIntegral c .&. 0xF)) (fromIntegral pr) 0-> midiEvent (PitchWheel c pb) = Just $ PMMsg (224 .|. (fromIntegral c .&. 0xF)) (fromIntegral lo) (fromIntegral hi)-> where (hi,lo) = (pb `shiftR` 8, pb .&. 0xFF)-> midiEvent _ = Nothing ---A conversion function from PortMidi PMMsgs to Codec.Midi Messages.--> msgToMidi :: PMMsg -> Maybe Message-> msgToMidi (PMMsg m d1 d2) = -> let k = (m .&. 0xF0) `shiftR` 4-> c = fromIntegral (m .&. 0x0F)-> in case k of-> 0x8 -> Just $ NoteOff c (fromIntegral d1) (fromIntegral d2)-> 0x9 -> Just $ NoteOn c (fromIntegral d1) (fromIntegral d2)-> 0xA -> Just $ KeyPressure c (fromIntegral d1) (fromIntegral d2)-> 0xB -> Just $ ControlChange c (fromIntegral d1) (fromIntegral d2)-> 0xC -> Just $ ProgramChange c (fromIntegral d1)-> 0xD -> Just $ ChannelPressure c (fromIntegral d1)-> 0xE -> Just $ PitchWheel c (fromIntegral (d1 + d2 `shiftL` 8))-> 0xF -> Nothing -- SysEx event not handled-> _ -> Nothing------------------------- | Error Reporting | ------------------------> reportError :: String -> PMError -> IO ()-> reportError prompt e = do-> err <- getErrorText e -> hPutStrLn stderr $ prompt ++ ": " ++ err----------------------------- | Unused Functions | -------------------------> -- Prints all DeviceInfo found by getAllDevices.-> printAllDeviceInfo :: IO ()-> printAllDeviceInfo = do-> (indevs, outdevs) <- getAllDevices-> mapM_ (print . snd) indevs-> mapM_ (print . snd) outdevs---- Given whether the device is an input device and the device name, --- returns the DeviceID.-getDeviceId :: Bool -> String -> IO (Maybe DeviceID)-getDeviceId isInput n = do- devs <- getAllDevices- return $ findIndex (\(_,d) -> name d == n && input d == isInput) devs--> playTrackRealTime :: OutputDeviceID -> [(t, Message)] -> IO ()-> playTrackRealTime device track = do-> out <- midiOutRealTime device-> case out of-> Nothing -> return ()-> Just (out, stop) -> finally (playTrack out track) stop-> where-> playTrack out [] = do-> t <- getTimeNow-> out (t, TrackEnd)-> playTrack out (e@(_, m) : s) = do-> t <- getTimeNow-> out (t, m) -> if isTrackEnd m -> then return ()-> else playTrack out s--> {--> ticksPerBeat = case division of-> TicksPerBeat n -> n-> TicksPerSecond mode nticks -> (256 - mode - 128) * nticks `div` 2 -> -}--> {--> runTrack tpb = runTrack' 0 0 120 -- 120 beat/s is the default tempo-> where-> runTrack' t t0 bps ((_, TempoChange tempo) : l) = -> let bps' = 1000000 `div` fromIntegral tempo -> in runTrack' t t0 bps' l-> runTrack' t t0 bps ((t1, m) : l) = -> let t' = t + 1000 * fromIntegral (t1 - t0) `div` (tpb * bps)-> in (t', m) : runTrack' t' t1 bps l-> runTrack' _ _ _ [] = [] ->-> playTrack s ch t0 = playTrack' 0-> where-> playTrack' t [] = putStrLn "done" >> putMVar ch Nothing >> return (round (t * 1.0E3))-> playTrack' _ ((t, e):es) = putMVar ch (Just io) >> playTrack' t es -> where -> io = case midiEvent e of-> Just m -> writeShort s (PMEvent m (t0 + round (t * 1.0E3)))-> Nothing -> return NoError -> -}--> recordMidi :: DeviceID -> (Track Time -> IO ()) -> IO ()-> recordMidi device f = do-> ch <- newChan-> final <- midiInRealTime device (\e -> writeChan ch e >> return False)-> case final of -> Nothing -> return () -> Just fin -> do-> track <- getChanContents ch-> done <- newEmptyMVar -> forkIO (f track >> putMVar done ()) -> putStrLn "Start recording, hit ENTER when you are done."-> getLine-> fin -> takeMVar done-> return ()--> midiInRealTime :: DeviceID -> ((Time, Message) -> IO Bool) -> IO (Maybe (IO ()))-> midiInRealTime device callback = do-> r <- openInput device -> case r of-> Right e -> reportError "midiInRealTime" e >> return Nothing -> Left s -> do-> fin <- newEmptyMVar-> forkIO (loop Nothing s fin)-> return (Just (putMVar fin () >> putMVar fin ()))-> where-> loop start s fin = do-> done <- tryTakeMVar fin-> t <- getTimeNow-> case done of-> Just _ -> close s >> callback (t, TrackEnd) >> takeMVar fin >> return ()-> Nothing -> do-> e <- readEvents s-> case e of-> Right e -> if e == NoError -> then threadDelay 1000 >> loop start s fin-> else do-> reportError "midiInRealTime" e -> callback (t, TrackEnd)-> return ()-> Left l -> do-> t <- getTimeNow-> sendEvts start t l-> where -> sendEvts start now [] = loop start s fin-> sendEvts start now (e@(PMEvent m t):l) = do-> let t0 = maybe t id start-> case msgToMidi m of-> Just m' -> do-> done <- callback (now + fromIntegral (t - t0) / 1E3, m')-> if done then close s >> return () else sendEvts (Just t0) now l-> Nothing -> sendEvts (Just t0) now l-+ +> {-# LANGUAGE GeneralizedNewtypeDeriving #-} +> module Euterpea.IO.MIDI.MidiIO ( +> getAllDevices, --isValidInputDevice, isValidOutputDevice, -- Used only by Euterpea.IO.MUI.MidiWidgets +> terminateMidi, initializeMidi, -- Used only by Euterpea.IO.MUI +> outputMidi, deliverMidiEvent, -- Used only by Euterpea.IO.MUI.MidiWidgets (particularly by midiOut) +> pollMidi, -- Used only by Euterpea.IO.MUI.MidiWidgets (particularly by midiIn) +> defaultOutput, defaultInput, +> playMidi, +> MidiMessage (ANote, Std), +> getTimeNow, +> DeviceInfo(..), InputDeviceID, OutputDeviceID, Message(..), Time, +> unsafeInputID, unsafeOutputID, +> ) where + +> import Codec.Midi (Time, Channel, Key, Velocity, +> Message (..), Midi (..), Track, +> toRealTime, toAbsTime, toSingleTrack, isTrackEnd) +> import Sound.PortMidi (DeviceInfo (..), getDeviceInfo, +> DeviceID, countDevices, time, +> getDefaultOutputDeviceID, getDefaultInputDeviceID, +> openInput, openOutput, readEvents, +> close, writeShort, getErrorText, terminate, initialize, +> PMError (NoError, BufferOverflow), PMStream, +> PMEvent (..), PMMsg (PMMsg)) +> import Control.Exception (finally) +> import Control.Concurrent +> import Control.Concurrent.STM.TChan +> import Control.Monad.STM (atomically) +> import Data.IORef + +> import Data.Bits (shiftR, shiftL, (.|.), (.&.)) +> import Data.List (findIndex) +> import Data.Maybe (mapMaybe) +> import qualified Data.Heap as Heap + +> import System.IO (hPutStrLn, stderr) +> import System.IO.Unsafe (unsafePerformIO) +> import Control.DeepSeq (NFData) + + +---------------------------- + | Midi Type declarations | +---------------------------- + +> type MidiEvent = (Time, MidiMessage) + +> data MidiMessage = ANote { channel :: !Channel, key :: !Key, +> velocity :: !Velocity, duration :: !Time } +> | Std Message +> deriving Show + +> newtype InputDeviceID = InputDeviceID DeviceID +> deriving (Eq, Show, NFData) +> newtype OutputDeviceID = OutputDeviceID DeviceID +> deriving (Eq, Show, NFData) + +> unsafeInputID :: Int -> InputDeviceID +> unsafeInputID = InputDeviceID + +> unsafeOutputID :: Int -> OutputDeviceID +> unsafeOutputID = OutputDeviceID + +---------- + | Time | +---------- + +Is this the time we want? This comes from PortMidi, but there's also the +function FRP.UISF.SOE.timeGetTime which uses time data from GLFW. + +> getTimeNow :: IO Time +> getTimeNow = do +> t <- time +> return (fromIntegral t / 1000) + + +---------------------- + | Device Functions | +---------------------- + +getAllDevices returns a list of all of the DeviceInfos found. +It calls Port.Midi.getDeviceInfo over all device numbers + +> getAllDevices :: IO ([(InputDeviceID, DeviceInfo)], [(OutputDeviceID, DeviceInfo)]) +> getAllDevices = do +> n <- countDevices +> deviceInfos <- mapM getDeviceInfo [0..n-1] +> let devs = zip [0..n-1] deviceInfos +> return ([ (InputDeviceID d, i) | (d,i) <- devs, input i], +> [ (OutputDeviceID d, i) | (d,i) <- devs, output i]) + + +isValidInputDevice and isValideOutputDevice check whether the given +devices are respectively valid for input or output. + +isValidInputDevice, isValidOutputDevice :: DeviceID -> IO Bool +isValidInputDevice = isValidDevice input +isValidOutputDevice = isValidDevice output +isValidDevice :: (DeviceInfo -> Bool) -> DeviceID -> IO Bool +isValidDevice pred i = do + n <- countDevices + info <- getAllDevices + return $ + i >= 0 && i < n && pred (snd $ info !! i) + + +--------------------- + | Default devices | +--------------------- + +Rather than export the deviceIDs directly, these two functions allow +the caller to use the DeviceID without directly controlling it. + +They take a function (such as playMidi) and an auxiary argument and +apply them together with the default device. If no default device +exists, an error is thrown. + +> defaultOutput :: (OutputDeviceID -> a -> IO b) -> a -> IO b +> defaultOutput f a = do +> i <- getDefaultOutputDeviceID +> case i of +> Nothing -> error "No MIDI output device found" +> Just i -> f (OutputDeviceID i) a +> +> defaultInput :: (InputDeviceID -> a -> IO b) -> a -> IO b +> defaultInput f a = do +> i <- getDefaultInputDeviceID +> case i of +> Nothing -> error "No MIDI input device found" +> Just i -> f (InputDeviceID i) a + + +----------------------- + | Priority Channels | +----------------------- + +The priority channel data type and a constructor for it will be used +by devices. We define them here. + +> data PrioChannel a b = PrioChannel +> { get :: IO (Heap.MinPrioHeap a b), +> push :: a -> b -> IO (), +> pop :: IO (a,b), +> peek :: IO (Maybe (a,b)) } + +> makePriorityChannel :: IO (PrioChannel Time Message) +> makePriorityChannel = do +> heapRef <- newIORef (Heap.empty :: Heap.MinPrioHeap Time Message) +> let get = readIORef heapRef +> push a b = modifyIORef heapRef (Heap.insert (a,b)) +> pop = do +> h <- get +> let (a, h') = Heap.extractHead h +> modifyIORef heapRef (\_ -> h') +> return a +> peek = do +> h <- get +> if Heap.isEmpty h +> then return Nothing +> else return $ Just $ Heap.head h +> +> return $ PrioChannel get push pop peek + + +------------------------ + | Global Device Data | +------------------------ + +We keep a mapping from DeviceID to the priority channel for keeping +track of future MIDI messages, an output function to produce sound, +and a stop function. This mapping is stored in the global ref +outDevMap, and it is accessed by getOutDev (which looks up info +and adds associations if necessary) and terminateMidi (which calls +the stop function on all elements and clears the mapping). + +outDevMap is the global mapping. + +> outDevMap :: IORef [(OutputDeviceID, +> (PrioChannel Time Message, -- priority channel +> (Time, Message) -> IO (), -- sound output function +> IO ()))] -- stop/terminate function +> outDevMap = unsafePerformIO $ newIORef [] + + +outPort and inPort are global memory refs that contain a mapping of +DeviceID to Port Midi Streams. They are modified with addPort (which +adds a new mapping to the list) and lookupPort (which, given a DeviceID, +returns the Port Midi Stream associated with it). + +> outPort :: IORef [(OutputDeviceID, PMStream)] +> inPort :: IORef [(InputDeviceID, PMStream)] +> outPort = unsafePerformIO (newIORef []) +> inPort = unsafePerformIO (newIORef []) + +> lookupPort :: (Eq deviceid) => IORef [(deviceid, PMStream)] -> deviceid -> IO (Maybe PMStream) +> lookupPort p i = readIORef p >>= (return . lookup i) + +> addPort :: IORef [(deviceid, PMStream)] -> (deviceid, PMStream) -> IO () +> addPort p is = modifyIORef p (is:) + + +-------------------------------------------------- + | Global Device Initialization and Termination | +-------------------------------------------------- + +initializeMidi just initializes PortMidi + +> initializeMidi :: IO () +> initializeMidi = do +> e <- initialize +> if e == NoError +> then return () +> else reportError "initializeMidi" e + +terminateMidi calls the stop function on all elements of outDevMap +and clears the mapping entirely. It also clears outPort and inPort. + +> terminateMidi :: IO () +> terminateMidi = do +> inits <- readIORef outDevMap +> mapM_ (\(_, (_,_out,stop)) -> stop) inits +> terminate +> modifyIORef outDevMap (const []) +> writeIORef outPort [] +> writeIORef inPort [] + + +------------------- + | Device Lookup | +------------------- + +getOutDev looks up info in outDevMap and adds associations if necessary. +It is accessed as a helper function for outputMidi and deliverMidiEvent. + +> getOutDev :: OutputDeviceID -> IO (PrioChannel Time Message, (Time, Message) -> IO (), IO ()) +> getOutDev devId = do +> inits <- readIORef outDevMap +> case lookup devId inits of +> Just f -> return f +> Nothing -> do +> x <- midiOutRealTime' devId -- Changes made by Donya Quick: this line used to pattern match against Just. +> pChan <- makePriorityChannel +> case x of Just (mout,stop) -> do -- Case statement added. +> modifyIORef outDevMap ((devId,(pChan,mout,stop)):) +> return (pChan,mout,stop) +> Nothing -> return (pChan, const (return ()), return ()) -- Nothing case added + + +---------------- + | Midi Input | +---------------- + +pollMidi take an input device and a callback function and polls the device +for midi events. Any events are sent, along with the current time, to +the callback function. +DWC NOTE: Why is the time even used? All messages get the same time? + +> pollMidiCB :: InputDeviceID -> ((Time, [Message]) -> IO ()) -> IO () +> pollMidiCB idid@(InputDeviceID devId) callback = do +> s <- lookupPort inPort idid +> case s of +> Nothing -> do +> r <- openInput devId +> case r of +> Right e -> reportError "pollMidiCB" e +> Left s -> addPort inPort (idid, s) >> input s +> Just s -> input s +> where +> input :: PMStream -> IO () +> input s = do +> e <- readEvents s +> case e of +> Right e -> if e == NoError +> then return () +> else reportError "pollMidiCB" e +> Left l -> do +> now <- getTimeNow +> case mapMaybe (msgToMidi . message) l of +> [] -> return () +> ms -> callback (now, ms) + +> pollMidi :: InputDeviceID -> IO (Maybe (Time, [Message])) +> pollMidi idid@(InputDeviceID devId) = do +> s <- lookupPort inPort idid +> case s of +> Nothing -> do +> r <- openInput devId +> case r of +> Right e -> reportError "pollMIDI" e >> return Nothing +> Left s -> addPort inPort (idid, s) >> input s +> Just s -> input s +> where +> input :: PMStream -> IO (Maybe (Time, [Message])) +> input s = do +> e <- readEvents s +> case e of +> Right e -> if e == NoError +> then return Nothing +> else reportError "pollMIDI" e >> return Nothing +> Left l -> do +> now <- getTimeNow +> case mapMaybe (msgToMidi . message) l of +> [] -> return Nothing +> ms -> return $ Just (now, ms) + + +--------------------------------------------- + | Midi Output for inidividual Midi events | +--------------------------------------------- + +The following two functions are for sending and playing individual +Midi events to devices. Typically, usage will be to call outputMidi +to play anything that's ready to play and then send in the latest +messages with deliverMidiEvent. Of course, if no new messages are +ready to be delivered, that step can be omitted. Either way, +outputMidi should be called many times per second to assure that +all Midi messages are played approximately when scheduled. + +deliverMidiEvent sends the given MidiEvent to the given device. If +the event is scheduled to happen ``now'', then it is immediately +played. Otherwise, it is queued for later. + +> deliverMidiEvent :: OutputDeviceID -> MidiEvent -> IO () +> deliverMidiEvent devId (t,m) = do +> (pChan, out, _stop) <- getOutDev devId +> now <- getTimeNow +> let deliver t m = do +> if t == 0 +> then out (now,m) +> else push pChan (now+t) m +> +> case m of +> Std m -> deliver t m +> ANote c k v d -> do +> deliver t (NoteOn c k v) +> deliver (t+d) (NoteOff c k v) + + +outputMidi plays all midi events that are waiting in this device's +priority queue whose time to play has come. + +> outputMidi :: OutputDeviceID -> IO () +> outputMidi devId = do +> (pChan, out, _stop) <- getOutDev devId +> let loop = do +> r <- peek pChan +> case r of +> Nothing -> return () +> Just (t,m) -> do +> now <- getTimeNow +> if t <= now +> then out (now, m) >> pop pChan >> loop +> else return () +> loop +> return () + + +------------------------------------------- + | Midi Output for a complete Midi track | +------------------------------------------- + +When an entire Midi track is ready to be played, the playMidi function +may be more appropriate than deliverMidiEvent and outputMidi. + +playMidi will queue up the entire Midi track given to it and then close +the output device. + +> playMidi :: OutputDeviceID -> Midi -> IO () +> playMidi device midi@(Midi _ division _) = do +> let track = toRealTime division (toAbsTime (head (tracks (toSingleTrack midi)))) +> out <- midiOutRealTime device +> case out of +> Nothing -> return () +> Just (out, stop) -> do +> t0 <- getTimeNow +> finally (playTrack t0 0 out track) stop +> where +> playTrack t0 t' out [] = out (t0 + t', TrackEnd) +> playTrack t0 t' out (e@(t, m) : s) = do +> out (t0 + t, m) +> if isTrackEnd m +> then return () +> else playTrack t0 t out s + + +--------------------- + | midiOutRealTime | +--------------------- + +The following two functions are used to open a device for Midi output. +They should only be called when the device hasn't yet been opened, and +they both return a ``play'' function and a ``stop'' function. + +Currently, midiOutRealTime' is used for Midi output for inidividual +Midi events, and midiOutRealTime is used for Midi output for a complete +Midi track. + +DWC Notes: +I'm not entirely sure how they both work yet. midiOutRealTime' +actually looks pretty straightforward in that it just creates the process +and stop functions and adds this device to the outPort device list. The +process function will look up the device in the outPort device list, and +if it finds it, it writes the message to it. The stop function removes +the device from the outPort list and closes it. + +On the other hand, midiOutRealTime spawns a new thread and does some +concurrent stuff. Really, it looks similar, but I don't know when to +use one and when to use the other. + +> midiOutRealTime' :: OutputDeviceID -> IO (Maybe ((Time, Message) -> IO (), IO ())) +> midiOutRealTime' odid@(OutputDeviceID devId) = do +> s <- openOutput devId 1 +> case s of +> Right e -> reportError "Unable to open output device in midiOutRealTime'" e >> return Nothing +> Left s -> do +> addPort outPort (odid, s) +> return $ Just (process odid, finalize odid) +> where +> process odid (t, msg) = do +> s <- lookupPort outPort odid +> case s of +> Nothing -> error ("midiOutRealTime': port " ++ show odid ++ " is not open for output") +> Just s -> do +> if isTrackEnd msg +> then return () +> else case midiEvent msg of +> Just m -> writeMsg s t m +> Nothing -> return () +> writeMsg s t m = do +> e <- writeShort s (PMEvent m (round (t * 1e3))) +> case e of +> NoError -> return () +> _ -> reportError "midiOutRealTime'" e +> finalize odid = do +> s <- lookupPort outPort odid +> e <- maybe (return NoError) close s +> case e of +> NoError -> return () +> _ -> reportError "midiOutRealTime'" e + + +> midiOutRealTime :: OutputDeviceID -> IO (Maybe ((Time, Message) -> IO (), IO ())) +> midiOutRealTime odid@(OutputDeviceID devId) = do +> s <- openOutput devId 1 +> case s of +> Right e -> reportError "outputMidi" e >> return Nothing +> Left s -> do +> ch <- atomically newTChan +> wait <- newEmptyMVar +> fin <- newEmptyMVar +> forkIO (pump s ch wait fin) +> return $ Just (output s ch wait, stop ch fin) +> where +> stop ch fin = atomically (unGetTChan ch Nothing) >> takeMVar fin +> output s ch wait evt@(_, m) = do +> atomically $ writeTChan ch (Just evt) +> if isTrackEnd m then takeMVar wait else return () +> pump s ch wait fin = loop +> where +> loop = do +> e <- atomically $ readTChan ch +> case e of +> Nothing -> close s >> putMVar fin () +> Just (t, msg) -> do +> now <- getTimeNow +> if (t > now + 5) +> then atomically (unGetTChan ch e) >> threadDelay 10000 >> loop +> else do +> done <- process t msg +> if done +> then waitUntil (t + 1) +> else loop +> where +> waitUntil t = do +> now <- getTimeNow +> if t > now +> then do +> threadDelay $ min 10000 (round((t - now) * 1E6)) +> empty <- atomically $ isEmptyTChan ch +> if empty +> then waitUntil t +> else do +> e <- atomically $ readTChan ch +> case e of +> Nothing -> finishup +> _ -> waitUntil t +> else finishup +> finishup = putMVar wait () >> close s >> putMVar fin () +> process t msg = if isTrackEnd msg +> then return True +> else case midiEvent msg of +> Just m -> writeMsg t m +> Nothing -> return False +> writeMsg t m = do +> e <- writeShort s (PMEvent m (round (t * 1e3))) +> case e of +> NoError -> return False +> BufferOverflow -> putStrLn "overflow" >> threadDelay 10000 >> writeMsg t m +> _ -> reportError "outputMidi" e >> return True + + +--------------------- + | MIDI Conversion | +--------------------- + +A conversion function from Codec.Midi Messages to PortMidi PMMsgs. + +> midiEvent :: Message -> Maybe PMMsg +> midiEvent (NoteOff c p v) = Just $ PMMsg (128 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral v) +> midiEvent (NoteOn c p v) = Just $ PMMsg (144 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral v) +> midiEvent (KeyPressure c p pr) = Just $ PMMsg (160 .|. (fromIntegral c .&. 0xF)) (fromIntegral p) (fromIntegral pr) +> midiEvent (ControlChange c cn cv) = Just $ PMMsg (176 .|. (fromIntegral c .&. 0xF)) (fromIntegral cn) (fromIntegral cv) +> midiEvent (ProgramChange c pn) = Just $ PMMsg (192 .|. (fromIntegral c .&. 0xF)) (fromIntegral pn) 0 +> midiEvent (ChannelPressure c pr) = Just $ PMMsg (208 .|. (fromIntegral c .&. 0xF)) (fromIntegral pr) 0 +> midiEvent (PitchWheel c pb) = Just $ PMMsg (224 .|. (fromIntegral c .&. 0xF)) (fromIntegral lo) (fromIntegral hi) +> where (hi,lo) = (pb `shiftR` 8, pb .&. 0xFF) +> midiEvent _ = Nothing + + +A conversion function from PortMidi PMMsgs to Codec.Midi Messages. + +> msgToMidi :: PMMsg -> Maybe Message +> msgToMidi (PMMsg m d1 d2) = +> let k = (m .&. 0xF0) `shiftR` 4 +> c = fromIntegral (m .&. 0x0F) +> in case k of +> 0x8 -> Just $ NoteOff c (fromIntegral d1) (fromIntegral d2) +> 0x9 -> Just $ NoteOn c (fromIntegral d1) (fromIntegral d2) +> 0xA -> Just $ KeyPressure c (fromIntegral d1) (fromIntegral d2) +> 0xB -> Just $ ControlChange c (fromIntegral d1) (fromIntegral d2) +> 0xC -> Just $ ProgramChange c (fromIntegral d1) +> 0xD -> Just $ ChannelPressure c (fromIntegral d1) +> 0xE -> Just $ PitchWheel c (fromIntegral (d1 + d2 `shiftL` 8)) +> 0xF -> Nothing -- SysEx event not handled +> _ -> Nothing + + +--------------------- + | Error Reporting | +--------------------- + +> reportError :: String -> PMError -> IO () +> reportError prompt e = do +> err <- getErrorText e +> hPutStrLn stderr $ prompt ++ ": " ++ err + + + + + +---------------------- + | Unused Functions | +---------------------- + +> -- Prints all DeviceInfo found by getAllDevices. +> printAllDeviceInfo :: IO () +> printAllDeviceInfo = do +> (indevs, outdevs) <- getAllDevices +> mapM_ (print . snd) indevs +> mapM_ (print . snd) outdevs + +-- Given whether the device is an input device and the device name, +-- returns the DeviceID. +getDeviceId :: Bool -> String -> IO (Maybe DeviceID) +getDeviceId isInput n = do + devs <- getAllDevices + return $ findIndex (\(_,d) -> name d == n && input d == isInput) devs + +> playTrackRealTime :: OutputDeviceID -> [(t, Message)] -> IO () +> playTrackRealTime device track = do +> out <- midiOutRealTime device +> case out of +> Nothing -> return () +> Just (out, stop) -> finally (playTrack out track) stop +> where +> playTrack out [] = do +> t <- getTimeNow +> out (t, TrackEnd) +> playTrack out (e@(_, m) : s) = do +> t <- getTimeNow +> out (t, m) +> if isTrackEnd m +> then return () +> else playTrack out s + +> {- +> ticksPerBeat = case division of +> TicksPerBeat n -> n +> TicksPerSecond mode nticks -> (256 - mode - 128) * nticks `div` 2 +> -} + +> {- +> runTrack tpb = runTrack' 0 0 120 -- 120 beat/s is the default tempo +> where +> runTrack' t t0 bps ((_, TempoChange tempo) : l) = +> let bps' = 1000000 `div` fromIntegral tempo +> in runTrack' t t0 bps' l +> runTrack' t t0 bps ((t1, m) : l) = +> let t' = t + 1000 * fromIntegral (t1 - t0) `div` (tpb * bps) +> in (t', m) : runTrack' t' t1 bps l +> runTrack' _ _ _ [] = [] +> +> playTrack s ch t0 = playTrack' 0 +> where +> playTrack' t [] = putStrLn "done" >> putMVar ch Nothing >> return (round (t * 1.0E3)) +> playTrack' _ ((t, e):es) = putMVar ch (Just io) >> playTrack' t es +> where +> io = case midiEvent e of +> Just m -> writeShort s (PMEvent m (t0 + round (t * 1.0E3))) +> Nothing -> return NoError +> -} + +> recordMidi :: DeviceID -> (Track Time -> IO ()) -> IO () +> recordMidi device f = do +> ch <- newChan +> final <- midiInRealTime device (\e -> writeChan ch e >> return False) +> case final of +> Nothing -> return () +> Just fin -> do +> track <- getChanContents ch +> done <- newEmptyMVar +> forkIO (f track >> putMVar done ()) +> putStrLn "Start recording, hit ENTER when you are done." +> getLine +> fin +> takeMVar done +> return () + +> midiInRealTime :: DeviceID -> ((Time, Message) -> IO Bool) -> IO (Maybe (IO ())) +> midiInRealTime device callback = do +> r <- openInput device +> case r of +> Right e -> reportError "midiInRealTime" e >> return Nothing +> Left s -> do +> fin <- newEmptyMVar +> forkIO (loop Nothing s fin) +> return (Just (putMVar fin () >> putMVar fin ())) +> where +> loop start s fin = do +> done <- tryTakeMVar fin +> t <- getTimeNow +> case done of +> Just _ -> close s >> callback (t, TrackEnd) >> takeMVar fin >> return () +> Nothing -> do +> e <- readEvents s +> case e of +> Right e -> if e == NoError +> then threadDelay 1000 >> loop start s fin +> else do +> reportError "midiInRealTime" e +> callback (t, TrackEnd) +> return () +> Left l -> do +> t <- getTimeNow +> sendEvts start t l +> where +> sendEvts start now [] = loop start s fin +> sendEvts start now (e@(PMEvent m t):l) = do +> let t0 = maybe t id start +> case msgToMidi m of +> Just m' -> do +> done <- callback (now + fromIntegral (t - t0) / 1E3, m') +> if done then close s >> return () else sendEvts (Just t0) now l +> Nothing -> sendEvts (Just t0) now l +
@@ -1,20 +1,20 @@-Copyright (c) 2008-2015 Euterpea authors--This software is provided 'as-is', without any express or implied-warranty. In no event will the authors be held liable for any damages-arising from the use of this software.--Permission is granted to anyone to use this software for any purpose,-including commercial applications, and to alter it and redistribute it-freely, subject to the following restrictions:--1. The origin of this software must not be misrepresented; you must not- claim that you wrote the original software. If you use this software- in a product, an acknowledgment in the product documentation would- be appreciated but is not required.--2. Altered source versions must be plainly marked as such, and must not- be misrepresented as being the original software.--3. This notice may not be removed or altered from any source- distribution.+Copyright (c) 2008-2015 Euterpea authors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would + be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution.
@@ -1,38 +1,38 @@-Version 2.0.0-Last modified: 31-Dec-2015- _____ _ -| ___| | | -| |__ _ _| |_ ___ _ __ _ __ ___ __ _ -| __| | | | __/ _ \ '__| '_ \ / _ \/ _` |-| |__| |_| | || __/ | | |_) | __/ (_| |-\____/\__,_|\__\___|_| | .__/ \___|\__,_|- | | - |_| --Authors:- Paul Hudak- Eric Cheng- Hai (Paul) Liu- Donya Quick - Dan Winograd-Cort --Maintainers:- Donya Quick <donyaquick@gmail.com>--Euterpea is a domain-specific language embedded in Haskell for -computer music research, education, and development, providing -both note-level and signal-level abstractions. It is a descendant -of Haskore and HasSound, and is intended for both educational purposes -as well as serious computer music applications. Euterpea is a -wide-spectrum DSL, suitable for high-level music representation, -algorithmic composition, and analysis; mid-level concepts such as -MIDI; and low-level audio processing, sound synthesis, and instrument -design. --See License for licensing information.--Please send questions, comments, and bug reports to Donya Quick:-donyaquick@gmail.com--Installation instructions are available at:+Version 2.0.0 +Last modified: 31-Dec-2015 + _____ _ +| ___| | | +| |__ _ _| |_ ___ _ __ _ __ ___ __ _ +| __| | | | __/ _ \ '__| '_ \ / _ \/ _` | +| |__| |_| | || __/ | | |_) | __/ (_| | +\____/\__,_|\__\___|_| | .__/ \___|\__,_| + | | + |_| + +Authors: + Paul Hudak + Eric Cheng + Hai (Paul) Liu + Donya Quick + Dan Winograd-Cort + +Maintainers: + Donya Quick <donyaquick@gmail.com> + +Euterpea is a domain-specific language embedded in Haskell for +computer music research, education, and development, providing +both note-level and signal-level abstractions. It is a descendant +of Haskore and HasSound, and is intended for both educational purposes +as well as serious computer music applications. Euterpea is a +wide-spectrum DSL, suitable for high-level music representation, +algorithmic composition, and analysis; mid-level concepts such as +MIDI; and low-level audio processing, sound synthesis, and instrument +design. + +See License for licensing information. + +Please send questions, comments, and bug reports to Donya Quick: +donyaquick@gmail.com + +Installation instructions are available at: http://www.euterpea.com
@@ -1,51 +1,51 @@-import Distribution.Simple-main = defaultMain---- January 18, 2014--- The following setup script uses the CCA preprocessor (ccap) to preprocess --- certain *.as files in the Euterpea code base. As of January 18, 2014, only --- one file is being preprocessed in this way (Euterpea.IO.Audio.Basics), and --- as some users have had difficulty with installations due to this --- preprocessing step, we are removing it from the installation procedure.--- --- Now, to process *.as files, one can directly use the ArrowWrap module in --- Euterpea. In ArrowWrap, all files to be preprocessed must be declared in --- the list called fileList. Then, simply run main.--- --- If this preprocessor is going to be reenabled, or if ArrowWrap is going --- to be used, one must either add haskell-src-exts >= 1.14.0 to the cabal --- build-depends or just install it directly.-{--module Main (main) where--import Distribution.Simple-import Distribution.Simple.PreProcess-import Distribution.Simple.Program-import Distribution.Simple.Utils-import System.Exit--import ArrowWrap--findArrowP verbosity = do- a <- findProgramLocation verbosity "ccap"- case a of - Nothing -> error "Preprocessor ccap not found. Please make sure the \- \CCA library is already installed, and ccap is in \- \your PATH environment."- Just p -> return p--ppArrow bi lbi = PreProcessor {- platformIndependent = True,- runPreProcessor = - mkSimplePreProcessor $ \inFile outFile verbosity ->- do info verbosity (inFile ++ " has been preprocessed to " ++ outFile)- arrowp <- findArrowP verbosity- runArrowP arrowp inFile outFile- }--myHooks = simpleUserHooks - { hookedPreProcessors = ("as", ppArrow) : knownSuffixHandlers }--main :: IO ()-main = defaultMainWithHooks myHooks+import Distribution.Simple +main = defaultMain + +-- January 18, 2014 +-- The following setup script uses the CCA preprocessor (ccap) to preprocess +-- certain *.as files in the Euterpea code base. As of January 18, 2014, only +-- one file is being preprocessed in this way (Euterpea.IO.Audio.Basics), and +-- as some users have had difficulty with installations due to this +-- preprocessing step, we are removing it from the installation procedure. +-- +-- Now, to process *.as files, one can directly use the ArrowWrap module in +-- Euterpea. In ArrowWrap, all files to be preprocessed must be declared in +-- the list called fileList. Then, simply run main. +-- +-- If this preprocessor is going to be reenabled, or if ArrowWrap is going +-- to be used, one must either add haskell-src-exts >= 1.14.0 to the cabal +-- build-depends or just install it directly. +{- +module Main (main) where + +import Distribution.Simple +import Distribution.Simple.PreProcess +import Distribution.Simple.Program +import Distribution.Simple.Utils +import System.Exit + +import ArrowWrap + +findArrowP verbosity = do + a <- findProgramLocation verbosity "ccap" + case a of + Nothing -> error "Preprocessor ccap not found. Please make sure the \ + \CCA library is already installed, and ccap is in \ + \your PATH environment." + Just p -> return p + +ppArrow bi lbi = PreProcessor { + platformIndependent = True, + runPreProcessor = + mkSimplePreProcessor $ \inFile outFile verbosity -> + do info verbosity (inFile ++ " has been preprocessed to " ++ outFile) + arrowp <- findArrowP verbosity + runArrowP arrowp inFile outFile + } + +myHooks = simpleUserHooks + { hookedPreProcessors = ("as", ppArrow) : knownSuffixHandlers } + +main :: IO () +main = defaultMainWithHooks myHooks -}