diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d828aea --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "rodio"] + path = rodio + url = https://github.com/Alch-Emi/rodio.git + branch = new-from-format-reader diff --git a/Cargo.lock b/Cargo.lock index edd83b2..79900c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,28 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "alsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.20.2", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "andrew" version = "0.3.1" @@ -72,9 +94,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "269d0f5e68353a7cab87f81e7c736adc008d279a36ebc6a05dfe01193a89f0c9" [[package]] name = "ash" @@ -207,6 +229,25 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -286,6 +327,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + [[package]] name = "cache-padded" version = "1.2.0" @@ -321,6 +368,21 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + [[package]] name = "cfg-expr" version = "0.8.1" @@ -348,6 +410,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "clang-sys" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa785e9017cb8e8c8045e3f096b7d1ebc4d7337cceccdca8d678a27f788ac133" +dependencies = [ + "glob", + "libc", + "libloading 0.6.7", +] + [[package]] name = "clipboard-win" version = "4.2.2" @@ -435,6 +508,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -544,6 +627,50 @@ dependencies = [ "objc", ] +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.3", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "nix 0.20.2", + "oboe", + "parking_lot", + "stdweb", + "thiserror", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "crc32fast" version = "1.3.0" @@ -669,6 +796,7 @@ dependencies = [ "iced_native", "image", "rfd", + "rodio", "symphonia", ] @@ -1306,6 +1434,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "glow" version = "0.7.2" @@ -1671,6 +1805,20 @@ dependencies = [ "either", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1843,6 +1991,15 @@ dependencies = [ "sid", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1997,7 +2154,32 @@ checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" dependencies = [ "jni-sys", "ndk-sys", - "num_enum", + "num_enum 0.4.3", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum 0.5.6", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum 0.5.6", "thiserror", ] @@ -2010,7 +2192,35 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.2.1", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", "ndk-macro", "ndk-sys", ] @@ -2022,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "quote", "syn", @@ -2083,6 +2293,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.0" @@ -2094,6 +2314,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -2152,7 +2383,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" dependencies = [ "derivative", - "num_enum_derive", + "num_enum_derive 0.4.3", +] + +[[package]] +name = "num_enum" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +dependencies = [ + "num_enum_derive 0.5.6", ] [[package]] @@ -2161,7 +2401,19 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +dependencies = [ + "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", @@ -2206,6 +2458,29 @@ dependencies = [ "objc", ] +[[package]] +name = "oboe" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +dependencies = [ + "jni", + "ndk 0.4.0", + "ndk-glue 0.4.0", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -2301,6 +2576,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2384,6 +2665,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -2511,6 +2802,21 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "rfd" version = "0.6.3" @@ -2534,6 +2840,14 @@ dependencies = [ "windows", ] +[[package]] +name = "rodio" +version = "0.14.0" +dependencies = [ + "cpal", + "symphonia", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2631,6 +2945,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "sid" version = "0.6.1" @@ -2779,6 +3099,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + [[package]] name = "storage-map" version = "0.3.0" @@ -2826,16 +3152,15 @@ checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" [[package]] name = "symphonia" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7" +checksum = "9fae959d5ea7b4cd0cd8db3b899ec4f549b0d8a298694826a36ae7e5f19d4aa6" dependencies = [ "lazy_static", "symphonia-bundle-flac", "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-pcm", - "symphonia-codec-vorbis", "symphonia-core", "symphonia-format-isomp4", "symphonia-format-ogg", @@ -2845,9 +3170,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "116e5412f5fb4e5d07efd6628d50d6fcd7a61ebef43d98f5012f3cf763b25d02" +checksum = "b237be42d0ff1ff64c6e073aea4f93985ca51de93b8279f16a4b006e3e5997af" dependencies = [ "log", "symphonia-core", @@ -2857,9 +3182,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790" +checksum = "b9f596fe16d2ae06e9404558644b61e27e2dcfee6c511f559be4699c403283fa" dependencies = [ "bitflags", "lazy_static", @@ -2870,9 +3195,9 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3d7ab37eb9b7df16ddedd7adb7cc382afe708ff078e525a14dc9b05e57558f" +checksum = "9e636422dccfb202b24f8066b8d3ebfa914bbc690dae78db52b54691ba916c3f" dependencies = [ "lazy_static", "log", @@ -2881,43 +3206,32 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1d54738758993546107e3a4be2c1da827f2d4489fcffee0fa47867254e44c7" +checksum = "021d8161b186bea81c7cf4a80c67c71fb53862cd9f426cd3e032ae09bdd42dec" dependencies = [ "log", "symphonia-core", ] -[[package]] -name = "symphonia-codec-vorbis" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa" -dependencies = [ - "log", - "symphonia-core", - "symphonia-utils-xiph", -] - [[package]] name = "symphonia-core" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9" +checksum = "c1742e06f50b4a7ed7abee53433231e050a248b498cd0ae2c639c8a70b115001" dependencies = [ - "arrayvec 0.7.2", + "arrayvec 0.6.1", "bitflags", - "bytemuck", + "byteorder", "lazy_static", "log", ] [[package]] name = "symphonia-format-isomp4" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feee3a7711e7ec1b7540756f3868bbb3cbb0d1195569b9bc26471a24a02f57b5" +checksum = "bf3c6b6ca3347caa22d72f04cfd509f0c32683b0dbe01d0cfb63fd2726ac6da5" dependencies = [ "encoding_rs", "log", @@ -2927,9 +3241,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519" +checksum = "4898eef85c5a05136e1f3b5ff1afe4423cd7642c8979ed007cc8924b9a4c18c1" dependencies = [ "log", "symphonia-core", @@ -2939,9 +3253,9 @@ dependencies = [ [[package]] name = "symphonia-format-wav" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3586e944a951f3ff19ae14d3f46643c063784f119bffb091fc536102909575" +checksum = "97e863d9a912ea518dfae664292e38b2b961a1eb780251eba89b60e3905fef37" dependencies = [ "log", "symphonia-core", @@ -2950,9 +3264,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a" +checksum = "db5e36e38a7400f968569135e7ac0f8647de42e93905ad41c79d583aaeae565c" dependencies = [ "encoding_rs", "lazy_static", @@ -2962,9 +3276,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9" +checksum = "47377d86d61acf4d5b1a054b8e7a7cac8266155577a5410e4d746aec6394c42a" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -3629,8 +3943,8 @@ dependencies = [ "log", "mio", "mio-extras", - "ndk", - "ndk-glue", + "ndk 0.2.1", + "ndk-glue 0.2.1", "ndk-sys", "objc", "parking_lot", @@ -3690,7 +4004,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" dependencies = [ - "nom", + "nom 7.1.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index df197df..daf2ef3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,9 @@ iced_native = "0.4.0" rfd = "0.6.3" [dependencies.symphonia] -# Music decoding, playing, and metadata parsing +# Music decoding and metadata parsing features = ["isomp4", "aac", "mp3"] -version = "0.4.0" +version = "0.3.0" [dependencies.iced] # Display windows & graphics @@ -33,3 +33,9 @@ version = "0.3.0" # Display windows & graphics features = ["smol"] version = "0.3.0" + +[dependencies.rodio] +# Playing audio +default-features = false +features = ["symphonia"] +path = "./rodio/" diff --git a/rodio b/rodio new file mode 160000 index 0000000..706abaf --- /dev/null +++ b/rodio @@ -0,0 +1 @@ +Subproject commit 706abafcadab2c0049d7a7724cae43f203ba0f67 diff --git a/src/app.rs b/src/app.rs index 3d1bc01..f4f79a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::controls::ControlsEvent; use crate::load_song::load_song; use crate::editor::Editor; use crate::file_select::FileSelector; @@ -34,6 +35,7 @@ pub enum Message { PromptForFile, FileOpened(PathBuf), Resized(u32, u32), + ControlsEvent(ControlsEvent), Null, } @@ -111,7 +113,12 @@ impl Application for DelyriumApp { if let Some(lyrics) = self.lyrics_component.as_mut() { lyrics.notify_resized(w, h); } - } + }, + Message::ControlsEvent(e) => { + if let Some(lyrics) = self.lyrics_component.as_mut() { + lyrics.handle_controls_event(e); + } + }, Message::Null => { }, } @@ -150,15 +157,21 @@ impl Application for DelyriumApp { } }); - let fps30 = time::every(Duration::from_millis(1000 / 30)).map(|_| Message::Tick); - - if self.lyrics_component.is_none() { - Subscription::batch([ - runtime_events, - fps30 - ]) + let is_animating = if let Some(editor) = &self.lyrics_component { + editor.is_animating() } else { - runtime_events - } + true + }; + + let fps30 = if is_animating { + time::every(Duration::from_millis(1000 / 30)).map(|_| Message::Tick) + } else { + Subscription::none() + }; + + Subscription::batch([ + runtime_events, + fps30 + ]) } } diff --git a/src/controls.rs b/src/controls.rs new file mode 100644 index 0000000..2c7781f --- /dev/null +++ b/src/controls.rs @@ -0,0 +1,139 @@ +use iced::Length; +use iced::Color; +use symphonia::core::formats::FormatReader; +use crate::player::PlayerError; +use iced::canvas::event::Status; +use iced::canvas::Event; +use crate::styles::Theme; +use iced::canvas::Frame; +use iced::canvas::Geometry; +use iced::canvas::Cursor; +use iced::Rectangle; +use crate::app::Message; +use iced::canvas::Program; +use iced::Canvas; +use iced::mouse::{self, Button}; +use crate::player::Player; + +#[derive(Debug, Clone, Copy)] +pub enum ControlsEvent { + SeekPosition(f32), + TogglePlay, +} + +enum ErrorState { + Error(PlayerError), + NoError { + player: Player, + has_device: bool + } +} + +use ErrorState::*; + +pub struct Controls { + error_state: ErrorState, +} + +impl Controls { + pub fn new(song: Box) -> Self { + match Player::new(song) { + Ok(player) => { + Controls { + error_state: NoError { + has_device: player.has_output_device(), + player, + } + } + }, + Err(e) => { + Controls { + error_state: Error(e) + } + } + } + } + + pub fn view_progress(&mut self, theme: Theme) -> Canvas { + Canvas::new((&*self, theme)) + .width(Length::Units(50)) + .height(Length::Fill) + } + + pub fn handle_event(&mut self, event: ControlsEvent) { + if let NoError { player, has_device } = &mut self.error_state { + let result = match event { + ControlsEvent::SeekPosition(pos) => player.seek_percentage(pos), + ControlsEvent::TogglePlay => player.toggle_play(), + }; + + match result { + Ok(now_has_device) => { + *has_device = now_has_device; + }, + Err(e) => { + self.error_state = Error(e); + }, + }; + } + } + + pub fn is_playing(&self) -> bool { + if let NoError { player, has_device: true } = &self.error_state { + player.is_playing() + } else { + false + } + } +} + +impl Program for (&Controls, Theme) { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { + let mut frame = Frame::new(bounds.size()); + + match &self.0.error_state { + NoError { player, has_device: true } => { + let mut background = self.1.text_color; + background.a = 0.2; + + frame.fill_rectangle(bounds.position(), bounds.size(), background); + + let mut played_size = bounds.size(); + played_size.height *= player.position_percent(); + + frame.fill_rectangle(bounds.position(), played_size, self.1.text_color); + }, + NoError { player: _, has_device: false } => { + let mut background = self.1.text_color; + background.a = 0.1; + + frame.fill_rectangle(bounds.position(), bounds.size(), background); + }, + Error(e) => { + let background = Color {r: 1., g: 0.1, b: 0.1, a: 1.}; + + frame.fill_rectangle(bounds.position(), bounds.size(), background); + eprintln!("Error!!! {}", e.to_string()); + } + } + + vec![frame.into_geometry()] + } + + fn update(&mut self, event: Event, bounds: Rectangle, cursor: Cursor) -> (Status, Option) { + match (event, cursor) { + (Event::Mouse(mouse::Event::ButtonReleased(Button::Left)), Cursor::Available(pos)) + if bounds.contains(pos) => { + let sought = (pos.y - bounds.position().y) / bounds.size().height; + + (Status::Captured, Some(Message::ControlsEvent(ControlsEvent::SeekPosition(sought)))) + }, + (Event::Mouse(mouse::Event::ButtonReleased(Button::Right)), Cursor::Available(pos)) + if bounds.contains(pos) => { + // TODO! This should be somewhere intuitive + (Status::Captured, Some(Message::ControlsEvent(ControlsEvent::TogglePlay))) + }, + _ => (Status::Ignored, None), + } + } +} diff --git a/src/editor.rs b/src/editor.rs index 3e74d12..6ac220f 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,3 +1,5 @@ +use crate::controls::ControlsEvent; +use crate::controls::Controls; use crate::lyrics::Lyric; use crate::lyrics::Lyrics; use crate::app::Message; @@ -18,7 +20,7 @@ use image::GenericImageView; pub struct Editor { lyrics: Lyrics, theme: Theme, - song: Box, + controls: Controls, left_peri: Periphery, rite_peri: Periphery, } @@ -40,13 +42,18 @@ impl Editor { let left_peri = Periphery::new(cover.clone(), true, size); let rite_peri = Periphery::new(cover, false, size); + let controls = Controls::new(song); + Self { lyrics: Lyrics::new(), - song, theme, left_peri, rite_peri, + controls, theme, left_peri, rite_peri, } } // TODO: work on untangling this mess + pub fn handle_controls_event(&mut self, event: ControlsEvent) { + self.controls.handle_event(event) + } pub fn insert_text(&mut self, text: String) { self.lyrics.insert_text(text); } @@ -60,6 +67,10 @@ impl Editor { self.lyrics.current_line_mut() } + pub fn is_animating(&self) -> bool { + self.controls.is_playing() + } + pub fn notify_resized(&mut self, w: u32, h: u32) { self.left_peri.notify_resized(w, h); self.rite_peri.notify_resized(w, h); @@ -70,6 +81,7 @@ impl Editor { Container::new( Row::new() .push(self.left_peri.view()) + .push(self.controls.view_progress(self.theme)) .push(self.lyrics.view(self.theme)) .push(self.rite_peri.view()) ) diff --git a/src/load_song.rs b/src/load_song.rs index 4aad798..19f750a 100644 --- a/src/load_song.rs +++ b/src/load_song.rs @@ -8,12 +8,10 @@ use std::ffi::OsStr; use symphonia::default; use symphonia::core::io::MediaSourceStream; use symphonia::core::probe::{Hint, ProbeResult}; -use symphonia::core::meta::MetadataRevision; use symphonia::core::meta::StandardVisualKey; use symphonia::core::formats::FormatReader; pub fn load_song(path: &Path) -> Result { - let codecs = default::get_codecs(); let probe = default::get_probe(); let file = OpenOptions::new() @@ -44,8 +42,9 @@ pub fn extract_cover(format: &mut dyn FormatReader) -> Option{ format.metadata() .current() .into_iter() - .flat_map(MetadataRevision::visuals) - .filter_map(|vis| image::load_from_memory(&vis.data).ok().map(|img| (vis.usage, img))) + // Replace this whole closure with MetadataRef::usage once we update + .flat_map(|meta| meta.visuals().iter().map(|v|(v.data.clone(), v.usage.clone())).collect::>()) + .filter_map(|(data, usage)| image::load_from_memory(&data).ok().map(|img| (usage, img))) .max_by_key(|(usage, _)| usage.map(|usage| match usage { StandardVisualKey::FrontCover => 00, diff --git a/src/main.rs b/src/main.rs index 9c6fcd5..66c68af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,8 @@ mod file_select; mod load_song; mod peripheries; mod editor; +mod player; +mod controls; fn main() { app::DelyriumApp::run(Settings::default()).unwrap(); diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..a40e8cd --- /dev/null +++ b/src/player.rs @@ -0,0 +1,257 @@ +use std::time::Instant; +use core::time::Duration; +use rodio::Decoder; +use symphonia::core::formats::FormatReader; +use rodio::source::Buffered; +use rodio::OutputStream; +use rodio::Sink; +use rodio::Source; + +use std::fmt; + +pub type Song = Buffered>; + +pub struct Player { + // [Buffered] is a pointer to a linked-list representation of the song, and so cloning + // it is as cheap as cloning an [Arc]. This should always point to the start of the + // song though, and so should not be changed after being initialized. + song: Song, + sink: Option<(Sink, OutputStream)>, + duration: Duration, + + /// The position of the playhead when playback started or was last stopped + start_position: Duration, + + /// The [`Instant`] playback started + playback_started: Option, +} + +impl Player { + + /// Create a new player from a song + pub fn new(song: Box) -> Result { + + let song = Decoder::new_from_format_reader(song) + .map_err(PlayerError::DecoderError)? + .buffered(); + + let duration = get_duration_of_song(song.clone()); + + let mut player = Player { + sink: None, + start_position: Duration::ZERO, + playback_started: None, + song, duration, }; + player.get_or_set_sink()?; + + Ok(player) + } + + /// Check if an audio sink is available + /// + /// Returns `true` if an output device is currently loaded, or `false` if a device was + /// not availble last time we tried to access one. + pub fn has_output_device(&self) -> bool { + self.sink.is_some() + } + + /// Attempt to re-request access to the output sink + /// + /// Returns `&self.sink` + /// + /// Can produce a [PlayerError::PlayError] + fn try_set_sink(&mut self) -> Result, PlayerError> { + self.sink = Some(OutputStream::try_default()) + .and_then(|result| + if let Err(&rodio::StreamError::NoDevice) = result.as_ref() { + None // This is okay and doesn't need to raise an error + } else { + Some(result) // We'll report this error + }) + .transpose() + .map_err(PlayerError::StreamError)? + .map(|(stream, handle)| Sink::try_new(&handle).map(|sink| (sink, stream))) + .transpose() + .map_err(PlayerError::PlayError)?; + + Ok(self.sink.as_ref().map(|(s, _)| s)) + } + + /// Return the current active sink, or attempt to request a new one + /// + /// Equivilent to calling [`try_set_sink()`] if the sink is missing + fn get_or_set_sink(&mut self) -> Result, PlayerError> { + if self.sink.is_some() { + Ok(self.sink.as_ref().map(|(s, _)| s)) + } else { + let song = self.song.clone(); + let out = self.try_set_sink(); + + if let Ok(Some(sink)) = out.as_ref() { + sink.pause(); + sink.append(song); + } + + out + } + } + + /// Attempt to resume playback, or start fresh + /// + /// Returns `true` if playback was resumed, `false` if there is no audio sink + /// available to start play, and [PlayerError] if there was a problem requesting + /// access to the audio sink. + pub fn play(&mut self) -> Result { + if let Some(sink) = self.get_or_set_sink()? { + sink.play(); + self.playback_started = Some(Instant::now()); + Ok(true) + } else { + Ok(false) + } + } + + /// Pause playback if playing, do nothing otherwise + /// + /// May be resumed later with [`play()`] + pub fn pause(&mut self) { + if let Some((sink, _)) = &self.sink { + sink.pause(); + self.start_position = self.position(); + self.playback_started = None; + } + } + + /// Returns true if the song is currently playing + /// + /// That is, if playback has started, has not been paused, and has not ended naturally + /// yet. + pub fn is_playing(&self) -> bool { + self.playback_started.is_some() && self.position() < self.duration + } + + /// Toggle playing + /// + /// This calls [`play()`] is the track is currently paused or stopped, and calls + /// [`pause()`] is the track is currently playing. If the track was already playing, + /// this will always return `Ok(true)`. + pub fn toggle_play(&mut self) -> Result { + if self.position() == self.duration { + self.seek(Duration::ZERO) + .and_then(|device| if device { self.play() } else { Ok(false) } ) + } else if self.playback_started.is_none() { + self.play() + } else { + self.pause(); + Ok(true) + } + } + + /// Return the duration of this track + /// + /// This is cheap and exact. All processing is done before hand. + pub fn get_duration(&self) -> Duration { + self.duration + } + + /// Attempt to seek to a given duration + /// + /// This is pretty expensive, since due to technical limitations, this means resetting + /// the player to the beginning before skipping the given duration. + /// + /// This can fail if there is no current output sink, and the attempt to access a + /// new one fails. If this is the case, false will be returned, unless the reason the + /// access failed was due to an error. + pub fn seek(&mut self, duration: Duration) -> Result { + + let was_stopped = self.sink + .as_mut() + .map(|(s, _)| s.is_paused() || s.empty()) + .unwrap_or(false); + + let song = self.song.clone(); + if let Some(sink) = self.try_set_sink()? { + sink.pause(); + sink.append( + song.skip_duration(duration) + ); + if was_stopped { + self.playback_started = None; + } else { + sink.play(); + self.playback_started = Some(Instant::now()); + } + self.start_position = duration; + Ok(true) + } else { + Ok(false) + } + } + + /// Seek to a specific percentage (out of 1.0) + /// + /// Performs a [`seek()`] operation, seeking to a given percentage of the song's full + /// length. See [`seek()`] for more details + pub fn seek_percentage(&mut self, percent: f32) -> Result { + self.seek( + self.duration.mul_f32(percent) + ) + } + + /// How far into the song the playback head currently is + /// + /// Computes the duration of the song that is before the playback head. This is + /// really an approximation based on how much time has elapsed since playback started, + /// but it should be close enough for almost all purposes. + pub fn position(&self) -> Duration { + self.duration.min( + self.start_position + + self.playback_started.map_or( + Duration::ZERO, + |ts| Instant::now() - ts, + ) + ) + } + + /// Computes the position as a fraction of the song duration + /// + /// 0.0 represents the beginning of the song, while 1.0 represents the end. See + /// [`position()`] for more information. + pub fn position_percent(&self) -> f32 { + // nightly: self.position().div_duration_f32(self.duration) + self.position().as_secs_f32() / self.duration.as_secs_f32() + } +} + +/// Manually calculate the exact length of a given song +/// +/// This is really expensive, and involves decoding and inefficiently counting the number +/// of samples in the song, but produces an exact output. Use sparingly. +fn get_duration_of_song(song: Song) -> Duration { + let sample_rate = song.sample_rate() as u64; + let n_channels = song.channels() as u64; + let n_samples = song.count() as u64; // expensive! + let n_whole_seconds = n_samples / n_channels / sample_rate; + let remaining_samples = n_samples % sample_rate; + let n_nanos = remaining_samples * 1_000_000_000 / sample_rate; + Duration::new(n_whole_seconds, n_nanos as u32) +} + +#[derive(Debug)] +pub enum PlayerError { + DecoderError(rodio::decoder::DecoderError), + PlayError(rodio::PlayError), + StreamError(rodio::StreamError), +} + +impl fmt::Display for PlayerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::DecoderError(e) => write!(f, "Could not decode the provided song: {}", e), + Self::PlayError(e) => write!(f, "Problem playing to the audio output: {}", e), + Self::StreamError(e) => write!(f, "Problem connecting to the audio output: {}", e), + } + } +} + +impl std::error::Error for PlayerError { }