commit dfb6008cebd4570d7df6b537d5d16cb1fea2572e Author: Tsiry Sandratraina Date: Wed Sep 7 23:59:24 2022 +0300 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccb5166 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7a15d2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1514 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "alsa" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[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 = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk", + "ndk-glue", + "nix", + "oboe", + "parking_lot 0.11.2", + "stdweb", + "thiserror", + "web-sys", + "winapi", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" + +[[package]] +name = "futures-util" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" + +[[package]] +name = "librespot-protocol" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d3ac6196ac0ea67bbe039f56d6730a5d8b31502ef9bce0f504ed729dcb39f" +dependencies = [ + "glob", + "protobuf", + "protobuf-codegen-pure", +] + +[[package]] +name = "lock_api" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "music-player" +version = "0.1.0" +dependencies = [ + "clap", + "cpal", + "futures-util", + "lazy_static", + "librespot-protocol", + "log", + "parking_lot 0.12.1", + "rand", + "rand_distr", + "rb", + "rodio", + "symphonia", + "thiserror", + "tokio", + "zerocopy", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-glue" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oboe" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "2.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" + +[[package]] +name = "protobuf-codegen" +version = "2.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec1632b7c8f2e620343439a7dfd1f3c47b18906c4be58982079911482b5d707" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protobuf-codegen-pure" +version = "2.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f8122fdb18e55190c796b088a16bdb70cd7acdcd48f7a8b796b58c62e532cc6" +dependencies = [ + "protobuf", + "protobuf-codegen", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rb" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56cf8381505b60ae18a4097f1d0be093287ca3bf4fbb23d36ac5ad3bba335daa" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rodio" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0939e9f626e6c6f1989adb6226a039c855ca483053f0ee7c98b90e41cf731e" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "minimp3", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "symphonia" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17033fe05e4f7f10a6ad602c272bafd2520b2e5cdd9feb61494d9cdce08e002f" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044f655337892b217d6df1d8336ee119414c3886c89c72e0156989cd2ad7934a" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5d3d53535ae2b7d0e39e82f683cac5398a6c8baca25ff1183e107d13959d3e" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9431b89428c31b01428563df18e52b1aff7c49e71fb80b2fa8e85632094776" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452438d6f32bf07f2f55f35371c3c8e9cce4a028a08b47fb301001f85a950f02" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cca412c954abda6ab62b5e51223568eb604ed8266ec777d99e1d63c608443c" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323b94435a1a807e1001e29490aeaef2660fb72b145d47497e8429a6cb1d67c3" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199a6417cd4115bac79289b64b859358ea050b7add0ceb364dc991f628c5b347" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f405400330b2cc0c70e19515198628ae9a6a99a59c77800541127d4cff113b96" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2f741469a0f103607ed1f2605f7f00b13ba044ea9ddc616764558c6d3d9b7d" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9d771aa6889f05b771e629110f6a60b5e1c0ad580fc41da574bc490fbe2822" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed71acf6b5e6e8bee1509597b86365a06b78c1d73218df47357620a6fe5997b" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbb0766ce77a8aef535f9438db645e7b6f1b2c4cf3be9bf246b4e11a7d5531" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "zerocopy" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a31649e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "music-player" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "3.2.20" +cpal = "0.13.0" +futures-util = "0.3.24" +lazy_static = "1.4.0" +librespot-protocol = "0.4.2" +log = "0.4.17" +parking_lot = "0.12.1" +rand = { version = "0.8.5", features = ["small_rng"] } +rand_distr = "0.4.3" +rb = "0.4.1" +rodio = { version = "0.15" } +symphonia = { version = "0.5.1", features = ["aac", "alac", "mp3"] } +thiserror = "1.0.34" +tokio = { version = "1.21.0", features = ["full"] } +zerocopy = "0.6.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf6b69d --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +## Music Player (written in Rust) + +

+ +

+ +Note: This is a work in progress. + +This is a simple music player that I made for my own use. It is not intended to be a full-featured music player, but rather a simple one that I can use to play music from my local hard drive. + +### Features + +- Play music from local hard drive +- Play music from a folder diff --git a/cover.svg b/cover.svg new file mode 100644 index 0000000..cafb73e --- /dev/null +++ b/cover.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addon/datpiff.rs b/src/addon/datpiff.rs new file mode 100644 index 0000000..95eb4c8 --- /dev/null +++ b/src/addon/datpiff.rs @@ -0,0 +1,7 @@ +use super::{Addon, StreamingAddon}; + +pub struct DatPiff {} + +impl Addon for DatPiff {} + +impl StreamingAddon for DatPiff {} diff --git a/src/addon/deezer.rs b/src/addon/deezer.rs new file mode 100644 index 0000000..ffb1cf5 --- /dev/null +++ b/src/addon/deezer.rs @@ -0,0 +1,7 @@ +use super::{Addon, StreamingAddon}; + +pub struct Deezer {} + +impl Addon for Deezer {} + +impl StreamingAddon for Deezer {} diff --git a/src/addon/genius.rs b/src/addon/genius.rs new file mode 100644 index 0000000..87cb23c --- /dev/null +++ b/src/addon/genius.rs @@ -0,0 +1,7 @@ +use super::{Addon, LyricsAddon}; + +pub struct Genius {} + +impl Addon for Genius {} + +impl LyricsAddon for Genius {} diff --git a/src/addon/local.rs b/src/addon/local.rs new file mode 100644 index 0000000..fc3026b --- /dev/null +++ b/src/addon/local.rs @@ -0,0 +1,7 @@ +use super::{Addon, StreamingAddon}; + +pub struct Local {} + +impl Addon for Local {} + +impl StreamingAddon for Local {} diff --git a/src/addon/mod.rs b/src/addon/mod.rs new file mode 100644 index 0000000..8645b4e --- /dev/null +++ b/src/addon/mod.rs @@ -0,0 +1,11 @@ +mod datpiff; +mod deezer; +mod genius; +mod local; +mod tononkira; + +pub trait Addon {} + +pub trait StreamingAddon {} + +pub trait LyricsAddon {} diff --git a/src/addon/tononkira.rs b/src/addon/tononkira.rs new file mode 100644 index 0000000..d6c7048 --- /dev/null +++ b/src/addon/tononkira.rs @@ -0,0 +1,7 @@ +use super::{Addon, LyricsAddon}; + +pub struct Tononkira {} + +impl Addon for Tononkira {} + +impl LyricsAddon for Tononkira {} diff --git a/src/audio_backend/mod.rs b/src/audio_backend/mod.rs new file mode 100644 index 0000000..391bd7c --- /dev/null +++ b/src/audio_backend/mod.rs @@ -0,0 +1,66 @@ +use thiserror::Error; + +use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket}; + +use self::rodio::RodioSink; + +#[derive(Debug, Error)] +pub enum SinkError { + #[error("Audio Sink Error Not Connected: {0}")] + NotConnected(String), + #[error("Audio Sink Error Connection Refused: {0}")] + ConnectionRefused(String), + #[error("Audio Sink Error On Write: {0}")] + OnWrite(String), + #[error("Audio Sink Error Invalid Parameters: {0}")] + InvalidParams(String), + #[error("Audio Sink Error Changing State: {0}")] + StateChange(String), +} + +pub type SinkResult = Result; + +pub trait Open { + fn open(_: Option, format: AudioFormat) -> Self; +} + +pub trait Sink { + fn start(&mut self) -> SinkResult<()> { + Ok(()) + } + fn stop(&mut self) -> SinkResult<()> { + Ok(()) + } + fn write( + &mut self, + packet: AudioPacket, + channels: u16, + sample_rate: u32, + converter: &mut Converter, + ) -> SinkResult<()>; +} + +pub type SinkBuilder = fn(Option, AudioFormat) -> Box; + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) +} + +pub mod rodio; + +pub mod sdl; + +pub const BACKENDS: &[(&str, SinkBuilder)] = &[ + (RodioSink::NAME, rodio::mk_rodio), // default goes first +]; + +pub fn find(name: Option) -> Option { + if let Some(name) = name { + BACKENDS + .iter() + .find(|backend| name == backend.0) + .map(|backend| backend.1) + } else { + BACKENDS.first().map(|backend| backend.1) + } +} diff --git a/src/audio_backend/rodio.rs b/src/audio_backend/rodio.rs new file mode 100644 index 0000000..6b2ba90 --- /dev/null +++ b/src/audio_backend/rodio.rs @@ -0,0 +1,222 @@ +use std::process::exit; +use std::thread; +use std::time::Duration; + +use cpal::traits::{DeviceTrait, HostTrait}; +use log::*; +use thiserror::Error; + +use super::{Sink, SinkError, SinkResult}; +use crate::config::AudioFormat; +use crate::convert::Converter; +use crate::decoder::AudioPacket; + +pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { + Box::new(open(cpal::default_host(), device, format)) +} + +#[derive(Debug, Error)] +pub enum RodioError { + #[error(" No Device Available")] + NoDeviceAvailable, + #[error(" device \"{0}\" is Not Available")] + DeviceNotAvailable(String), + #[error(" Play Error: {0}")] + PlayError(#[from] rodio::PlayError), + #[error(" Stream Error: {0}")] + StreamError(#[from] rodio::StreamError), + #[error(" Cannot Get Audio Devices: {0}")] + DevicesError(#[from] cpal::DevicesError), + #[error(" {0}")] + Samples(String), +} + +impl From for SinkError { + fn from(e: RodioError) -> SinkError { + use RodioError::*; + let es = e.to_string(); + match e { + StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es), + NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es), + DevicesError(_) => SinkError::InvalidParams(es), + } + } +} + +pub struct RodioSink { + rodio_sink: rodio::Sink, + format: AudioFormat, + _stream: rodio::OutputStream, +} + +fn list_formats(device: &rodio::Device) { + match device.default_output_config() { + Ok(cfg) => { + debug!(" Default config:"); + debug!(" {:?}", cfg); + } + Err(e) => { + // Use loglevel debug, since even the output is only debug + debug!("Error getting default rodio::Sink config: {}", e); + } + }; + + match device.supported_output_configs() { + Ok(mut cfgs) => { + if let Some(first) = cfgs.next() { + debug!(" Available configs:"); + debug!(" {:?}", first); + } else { + return; + } + + for cfg in cfgs { + debug!(" {:?}", cfg); + } + } + Err(e) => { + debug!("Error getting supported rodio::Sink configs: {}", e); + } + } +} + +fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> { + let mut default_device_name = None; + + if let Some(default_device) = host.default_output_device() { + default_device_name = default_device.name().ok(); + println!( + "Default Audio Device:\n {}", + default_device_name.as_deref().unwrap_or("[unknown name]") + ); + + list_formats(&default_device); + + println!("Other Available Audio Devices:"); + } else { + warn!("No default device was found"); + } + + for device in host.output_devices()? { + match device.name() { + Ok(name) if Some(&name) == default_device_name.as_ref() => (), + Ok(name) => { + println!(" {}", name); + list_formats(&device); + } + Err(e) => { + warn!("Cannot get device name: {}", e); + println!(" [unknown name]"); + list_formats(&device); + } + } + } + + Ok(()) +} + +fn create_sink( + host: &cpal::Host, + device: Option, +) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { + let rodio_device = match device.as_deref() { + Some("?") => match list_outputs(host) { + Ok(()) => exit(0), + Err(e) => { + error!("{}", e); + exit(1); + } + }, + Some(device_name) => { + host.output_devices()? + .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails + .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? + } + None => host + .default_output_device() + .ok_or(RodioError::NoDeviceAvailable)?, + }; + + let name = rodio_device.name().ok(); + info!( + "Using audio device: {}", + name.as_deref().unwrap_or("[unknown name]") + ); + + let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?; + let sink = rodio::Sink::try_new(&handle)?; + Ok((sink, stream)) +} + +pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { + info!( + "Using Rodio sink with format {:?} and cpal host: {}", + format, + host.id().name() + ); + + if format != AudioFormat::S16 && format != AudioFormat::F32 { + unimplemented!("Rodio currently only supports F32 and S16 formats"); + } + + let (sink, stream) = create_sink(&host, device).unwrap(); + + debug!("Rodio sink was created"); + RodioSink { + rodio_sink: sink, + format, + _stream: stream, + } +} + +impl Sink for RodioSink { + fn start(&mut self) -> SinkResult<()> { + self.rodio_sink.play(); + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.rodio_sink.sleep_until_end(); + self.rodio_sink.pause(); + Ok(()) + } + + fn write( + &mut self, + packet: AudioPacket, + channels: u16, + sample_rate: u32, + converter: &mut Converter, + ) -> SinkResult<()> { + let samples = packet + .samples() + .map_err(|e| RodioError::Samples(e.to_string()))?; + match self.format { + AudioFormat::F32 => { + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let source = rodio::buffer::SamplesBuffer::new(channels, sample_rate, samples_f32); + self.rodio_sink.append(source); + } + AudioFormat::S16 => { + let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(channels, sample_rate, samples_s16); + self.rodio_sink.append(source); + } + _ => unreachable!(), + }; + + // Chunk sizes seem to be about 256 to 3000 ish items long. + // Assuming they're on average 1628 then a half second buffer is: + // 44100 elements --> about 27 chunks + while self.rodio_sink.len() > 26 { + // sleep and wait for rodio to drain a bit + thread::sleep(Duration::from_millis(10)); + } + Ok(()) + } +} + +impl RodioSink { + #[allow(dead_code)] + pub const NAME: &'static str = "rodio"; +} diff --git a/src/audio_backend/sdl.rs b/src/audio_backend/sdl.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3ef263b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,47 @@ +use std::mem; +use std::str::FromStr; + +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum AudioFormat { + F64, + F32, + S32, + S24, + S24_3, + S16, +} + +impl FromStr for AudioFormat { + type Err = (); + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_ref() { + "F64" => Ok(Self::F64), + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), + } + } +} + +impl Default for AudioFormat { + fn default() -> Self { + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::F64 => mem::size_of::(), + Self::F32 => mem::size_of::(), + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), // S32 and S24 are both stored in i32 + } + } +} diff --git a/src/convert.rs b/src/convert.rs new file mode 100644 index 0000000..ca74ca3 --- /dev/null +++ b/src/convert.rs @@ -0,0 +1,121 @@ +use crate::dither::{Ditherer, DithererBuilder}; +use zerocopy::AsBytes; + +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn from_s24(sample: i32) -> Self { + // trim the padding in the most significant byte + #[allow(unused_variables)] + let [a, b, c, d] = sample.to_ne_bytes(); + #[cfg(target_endian = "little")] + return Self([a, b, c]); + #[cfg(target_endian = "big")] + return Self([b, c, d]); + } +} + +pub struct Converter { + ditherer: Option>, +} + +impl Converter { + pub fn new(dither_config: Option) -> Self { + match dither_config { + Some(ditherer_builder) => { + let ditherer = (ditherer_builder)(); + // info!("Converting with ditherer: {}", ditherer.name()); + Self { + ditherer: Some(ditherer), + } + } + None => Self { ditherer: None }, + } + } + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 32-bit signed integer, multiply by 2147483648 (0x80000000) and + /// saturate at the bounds of `i32`. + const SCALE_S32: f64 = 2147483648.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 24-bit signed integer, multiply by 8388608 (0x800000) and saturate + /// at the bounds of `i24`. + const SCALE_S24: f64 = 8388608.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 16-bit signed integer, multiply by 32768 (0x8000) and saturate at + /// the bounds of `i16`. When the samples were encoded using the same + /// scaling factor, like the reference Vorbis encoder does, this makes + /// conversions transparent. + const SCALE_S16: f64 = 32768.; + + pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { + // From the many float to int conversion methods available, match what + // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) + + // Casting float to integer rounds towards zero by default, i.e. it + // truncates, and that generates larger error than rounding to nearest. + match self.ditherer.as_mut() { + Some(d) => (sample * factor + d.noise()).round(), + None => (sample * factor).round(), + } + } + + // Special case for samples packed in a word of greater bit depth (e.g. + // S24): clamp between min and max to ensure that the most significant + // byte is zero. Otherwise, dithering may cause an overflow. This is not + // necessary for other formats, because casting to integer will saturate + // to the bounds of the primitive. + pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 { + let int_value = self.scale(sample, factor); + + // In two's complement, there are more negative than positive values. + let min = -factor; + let max = factor - 1.0; + + if int_value < min { + min + } else if int_value > max { + max + } else { + int_value + } + } + + pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { + samples.iter().map(|sample| *sample as f32).collect() + } + + pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) + .collect() + } + + // S24 is 24-bit PCM packed in an upper 32-bit word + pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) + .collect() + } + + // S24_3 is 24-bit PCM in a 3-byte array + pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)) + .collect() + } + + pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) + .collect() + } +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs new file mode 100644 index 0000000..6f5fa18 --- /dev/null +++ b/src/decoder/mod.rs @@ -0,0 +1,76 @@ +use std::ops::Deref; +use thiserror::Error; + +pub mod symphonia_decoder; + +#[derive(Error, Debug)] +pub enum DecoderError { + #[error("Symphonia Decoder Error: {0}")] + SymphoniaDecoder(String), +} + +pub type DecoderResult = Result; + +#[derive(Error, Debug)] +pub enum AudioPacketError { + #[error("Decoder Raw Error: Can't return Raw on Samples")] + Raw, + #[error("Decoder Samples Error: Can't return Samples on Raw")] + Samples, +} + +pub type AudioPacketResult = Result; + +pub enum AudioPacket { + Samples(Vec), + Raw(Vec), +} + +impl AudioPacket { + pub fn samples(&self) -> AudioPacketResult<&[f64]> { + match self { + AudioPacket::Samples(s) => Ok(s), + AudioPacket::Raw(_) => Err(AudioPacketError::Raw), + } + } + + pub fn raw(&self) -> AudioPacketResult<&[u8]> { + match self { + AudioPacket::Raw(d) => Ok(d), + AudioPacket::Samples(_) => Err(AudioPacketError::Samples), + } + } + + pub fn is_empty(&self) -> bool { + match self { + AudioPacket::Samples(s) => s.is_empty(), + AudioPacket::Raw(d) => d.is_empty(), + } + } +} + +#[derive(Debug, Clone)] +pub struct AudioPacketPosition { + pub position_ms: u32, + pub skipped: bool, +} + +impl Deref for AudioPacketPosition { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.position_ms + } +} + +pub trait AudioDecoder { + fn seek(&mut self, position_ms: u32) -> Result; + fn next_packet( + &mut self, + ) -> DecoderResult>; +} + +impl From for DecoderError { + fn from(err: symphonia::core::errors::Error) -> Self { + Self::SymphoniaDecoder(err.to_string()) + } +} diff --git a/src/decoder/symphonia_decoder.rs b/src/decoder/symphonia_decoder.rs new file mode 100644 index 0000000..7505b13 --- /dev/null +++ b/src/decoder/symphonia_decoder.rs @@ -0,0 +1,215 @@ +use std::io; + +use log::warn; +use symphonia::core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}, + errors::Error, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo, Track}, + io::{MediaSource, MediaSourceStream}, + meta::{MetadataOptions, Visual}, + probe::{Hint, ProbeResult}, + units::{Time, TimeBase}, +}; + +use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; + +use crate::PAGES_PER_MS; + +#[derive(Copy, Clone)] +struct PlayTrackOptions { + track_id: u32, + seek_ts: u64, +} + +fn first_supported_track(tracks: &[Track]) -> Option<&Track> { + tracks + .iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) +} + +pub struct SymphoniaDecoder { + format: Box, + decoder: Box, + sample_buffer: Option>, +} + +impl SymphoniaDecoder { + pub fn new(input: R, hint: Hint) -> DecoderResult + where + R: MediaSource + 'static, + { + // Create the media source stream using the boxed media source from above. + let mss = MediaSourceStream::new(Box::new(input), Default::default()); + + // Use the default options for format readers other than for gapless playback. + let format_opts = FormatOptions { + enable_gapless: false, + ..Default::default() + }; + + // Use the default options for metadata readers. + let metadata_opts: MetadataOptions = Default::default(); + + let track: Option = None; + + // Probe the media source stream for metadata and get the format reader. + match symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts) { + Ok(probed) => { + // Playback mode. + // print_format(song, &mut probed); + + // Set the decoder options. + let decode_opts = DecoderOptions { + verify: false, + ..Default::default() + }; + + // Play it! + // play(probed.format, track, seek_time, &decode_opts, no_progress); + + // If the user provided a track number, select that track if it exists, otherwise, select the + // first track with a known codec. + let track = track + .and_then(|t| probed.format.tracks().get(t)) + .or_else(|| first_supported_track(probed.format.tracks())); + + let track_id = match track { + Some(track) => track.id, + _ => { + return Err(DecoderError::SymphoniaDecoder( + "No supported tracks found".to_string(), + )) + } + }; + + let seek_ts = 0; + + let track_info = PlayTrackOptions { track_id, seek_ts }; + + // Get the selected track using the track ID. + let track = match probed + .format + .tracks() + .iter() + .find(|track| track.id == track_info.track_id) + { + Some(track) => track, + _ => { + return Err(DecoderError::SymphoniaDecoder( + "No supported tracks found".to_string(), + )) + } + }; + + // Create a decoder for the track. + let decoder = + symphonia::default::get_codecs().make(&track.codec_params, &decode_opts)?; + return Ok(SymphoniaDecoder { + format: probed.format, + decoder, + sample_buffer: None, + }); + } + Err(err) => { + // The input was not supported by any format reader. + panic!("file not supported. reason? {}", err); + } + } + } + + fn ts_to_ms(&self, ts: u64) -> u32 { + let time_base = self.decoder.codec_params().time_base; + let seeked_to_ms = match time_base { + Some(time_base) => { + let time = time_base.calc_time(ts); + (time.seconds as f64 + time.frac) * 1000. + } + // Fallback in the unexpected case that the format has no base time set. + None => ts as f64 * PAGES_PER_MS, + }; + seeked_to_ms as u32 + } +} + +impl AudioDecoder for SymphoniaDecoder { + fn seek(&mut self, position_ms: u32) -> Result { + let seconds = position_ms as u64 / 1000; + let frac = (position_ms as f64 % 1000.) / 1000.; + let time = Time::new(seconds, frac); + + // `track_id: None` implies the default track ID (of the container, not of Spotify). + let seeked_to_ts = self.format.seek( + SeekMode::Accurate, + SeekTo::Time { + time, + track_id: None, + }, + )?; + + // Seeking is a `FormatReader` operation, so the decoder cannot reliably + // know when a seek took place. Reset it to avoid audio glitches. + self.decoder.reset(); + + Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) + } + + fn next_packet( + &mut self, + ) -> DecoderResult> { + let mut skipped = false; + + loop { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(Error::IoError(err)) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } else { + return Err(DecoderError::SymphoniaDecoder(err.to_string())); + } + } + Err(err) => { + return Err(err.into()); + } + }; + + let position_ms = self.ts_to_ms(packet.ts()); + let packet_position = AudioPacketPosition { + position_ms, + skipped, + }; + + match self.decoder.decode(&packet) { + Ok(decoded) => { + let spec = *decoded.spec(); + let sample_buffer = match self.sample_buffer.as_mut() { + Some(buffer) => buffer, + None => { + let duration = decoded.capacity() as u64; + self.sample_buffer.insert(SampleBuffer::new(duration, spec)) + } + }; + + sample_buffer.copy_interleaved_ref(decoded); + let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); + + return Ok(Some(( + packet_position, + samples, + spec.channels.count() as u16, + spec.rate, + ))); + } + Err(Error::DecodeError(_)) => { + // The packet failed to decode due to corrupted or invalid data, get a new + // packet and try again. + warn!("Skipping malformed audio packet at {} ms", position_ms); + skipped = true; + continue; + } + Err(err) => return Err(err.into()), + } + } + } +} diff --git a/src/dither.rs b/src/dither.rs new file mode 100644 index 0000000..4b8a427 --- /dev/null +++ b/src/dither.rs @@ -0,0 +1,150 @@ +use rand::rngs::SmallRng; +use rand::SeedableRng; +use rand_distr::{Distribution, Normal, Triangular, Uniform}; +use std::fmt; + +use crate::NUM_CHANNELS; + +// Dithering lowers digital-to-analog conversion ("requantization") error, +// linearizing output, lowering distortion and replacing it with a constant, +// fixed noise level, which is more pleasant to the ear than the distortion. +// +// Guidance: +// +// * On S24, S24_3 and S24, the default is to use triangular dithering. +// Depending on personal preference you may use Gaussian dithering instead; +// it's not as good objectively, but it may be preferred subjectively if +// you are looking for a more "analog" sound akin to tape hiss. +// +// * Advanced users who know that they have a DAC without noise shaping have +// a third option: high-passed dithering, which is like triangular dithering +// except that it moves dithering noise up in frequency where it is less +// audible. Note: 99% of DACs are of delta-sigma design with noise shaping, +// so unless you have a multibit / R2R DAC, or otherwise know what you are +// doing, this is not for you. +// +// * Don't dither or shape noise on S32 or F32. On F32 it's not supported +// anyway (there are no integer conversions and so no rounding errors) and +// on S32 the noise level is so far down that it is simply inaudible even +// after volume normalisation and control. +// +pub trait Ditherer { + fn new() -> Self + where + Self: Sized; + fn name(&self) -> &'static str; + fn noise(&mut self) -> f64; +} + +impl fmt::Display for dyn Ditherer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +fn create_rng() -> SmallRng { + SmallRng::from_entropy() +} + +pub struct TriangularDitherer { + cached_rng: SmallRng, + distribution: Triangular, +} + +impl Ditherer for TriangularDitherer { + fn new() -> Self { + Self { + cached_rng: create_rng(), + // 2 LSB peak-to-peak needed to linearize the response: + distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + self.distribution.sample(&mut self.cached_rng) + } +} + +impl TriangularDitherer { + pub const NAME: &'static str = "tpdf"; +} + +pub struct GaussianDitherer { + cached_rng: SmallRng, + distribution: Normal, +} + +impl Ditherer for GaussianDitherer { + fn new() -> Self { + Self { + cached_rng: create_rng(), + // 1/2 LSB RMS needed to linearize the response: + distribution: Normal::new(0.0, 0.5).unwrap(), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + self.distribution.sample(&mut self.cached_rng) + } +} + +impl GaussianDitherer { + pub const NAME: &'static str = "gpdf"; +} + +pub struct HighPassDitherer { + active_channel: usize, + previous_noises: [f64; NUM_CHANNELS as usize], + cached_rng: SmallRng, + distribution: Uniform, +} + +impl Ditherer for HighPassDitherer { + fn new() -> Self { + Self { + active_channel: 0, + previous_noises: [0.0; NUM_CHANNELS as usize], + cached_rng: create_rng(), + distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + let new_noise = self.distribution.sample(&mut self.cached_rng); + let high_passed_noise = new_noise - self.previous_noises[self.active_channel]; + self.previous_noises[self.active_channel] = new_noise; + self.active_channel ^= 1; + high_passed_noise + } +} + +impl HighPassDitherer { + pub const NAME: &'static str = "tpdf_hp"; +} + +pub fn mk_ditherer() -> Box { + Box::new(D::new()) +} + +pub type DithererBuilder = fn() -> Box; + +pub fn find_ditherer(name: Option) -> Option { + match name.as_deref() { + Some(TriangularDitherer::NAME) => Some(mk_ditherer::), + Some(GaussianDitherer::NAME) => Some(mk_ditherer::), + Some(HighPassDitherer::NAME) => Some(mk_ditherer::), + _ => None, + } +} diff --git a/src/formatter.rs b/src/formatter.rs new file mode 100644 index 0000000..800201a --- /dev/null +++ b/src/formatter.rs @@ -0,0 +1,301 @@ +use std::fs::File; +use std::path::Path; + +use symphonia::core::formats::{Cue, FormatOptions, Track}; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::{ColorMode, MetadataOptions, MetadataRevision, Tag, Value, Visual}; +use symphonia::core::probe::Hint; +use symphonia::core::units::TimeBase; + +use log::info; + +pub fn print_format(path: &str) { + let mut hint = Hint::new(); + + let source = Box::new(File::open(Path::new(path)).unwrap()); + + // Provide the file extension as a hint. + if let Some(extension) = Path::new(path).extension() { + if let Some(extension_str) = extension.to_str() { + hint.with_extension(extension_str); + } + } + let mss = MediaSourceStream::new(source, Default::default()); + + let format_opts = FormatOptions { + enable_gapless: false, + ..Default::default() + }; + + let metadata_opts: MetadataOptions = Default::default(); + + let mut probed = symphonia::default::get_probe() + .format(&hint, mss, &format_opts, &metadata_opts) + .unwrap(); + + println!("+ {}", path); + print_tracks(probed.format.tracks()); + + // Prefer metadata that's provided in the container format, over other tags found during the + // probe operation. + if let Some(metadata_rev) = probed.format.metadata().current() { + print_tags(metadata_rev.tags()); + print_visuals(metadata_rev.visuals()); + + // Warn that certain tags are preferred. + if probed.metadata.get().as_ref().is_some() { + info!("tags that are part of the container format are preferentially printed."); + info!("not printing additional tags that were found while probing."); + } + } else if let Some(metadata_rev) = probed.metadata.get().as_ref().and_then(|m| m.current()) { + print_tags(metadata_rev.tags()); + print_visuals(metadata_rev.visuals()); + } + + print_cues(probed.format.cues()); + println!(":"); + println!(); +} + +fn print_update(rev: &MetadataRevision) { + print_tags(rev.tags()); + print_visuals(rev.visuals()); + println!(":"); + println!(); +} + +fn print_tracks(tracks: &[Track]) { + if !tracks.is_empty() { + println!("|"); + println!("| // Tracks //"); + + for (idx, track) in tracks.iter().enumerate() { + let params = &track.codec_params; + + print!("| [{:0>2}] Codec: ", idx + 1); + + if let Some(codec) = symphonia::default::get_codecs().get_codec(params.codec) { + println!("{} ({})", codec.long_name, codec.short_name); + } else { + println!("Unknown (#{})", params.codec); + } + + if let Some(sample_rate) = params.sample_rate { + println!("| Sample Rate: {}", sample_rate); + } + if params.start_ts > 0 { + if let Some(tb) = params.time_base { + println!( + "| Start Time: {} ({})", + fmt_time(params.start_ts, tb), + params.start_ts + ); + } else { + println!("| Start Time: {}", params.start_ts); + } + } + if let Some(n_frames) = params.n_frames { + if let Some(tb) = params.time_base { + println!( + "| Duration: {} ({})", + fmt_time(n_frames, tb), + n_frames + ); + } else { + println!("| Frames: {}", n_frames); + } + } + if let Some(tb) = params.time_base { + println!("| Time Base: {}", tb); + } + if let Some(padding) = params.delay { + println!("| Encoder Delay: {}", padding); + } + if let Some(padding) = params.padding { + println!("| Encoder Padding: {}", padding); + } + if let Some(sample_format) = params.sample_format { + println!("| Sample Format: {:?}", sample_format); + } + if let Some(bits_per_sample) = params.bits_per_sample { + println!("| Bits per Sample: {}", bits_per_sample); + } + if let Some(channels) = params.channels { + println!("| Channel(s): {}", channels.count()); + println!("| Channel Map: {}", channels); + } + if let Some(channel_layout) = params.channel_layout { + println!("| Channel Layout: {:?}", channel_layout); + } + if let Some(language) = &track.language { + println!("| Language: {}", language); + } + } + } +} + +fn print_cues(cues: &[Cue]) { + if !cues.is_empty() { + println!("|"); + println!("| // Cues //"); + + for (idx, cue) in cues.iter().enumerate() { + println!("| [{:0>2}] Track: {}", idx + 1, cue.index); + println!("| Timestamp: {}", cue.start_ts); + + // Print tags associated with the Cue. + if !cue.tags.is_empty() { + println!("| Tags:"); + + for (tidx, tag) in cue.tags.iter().enumerate() { + if let Some(std_key) = tag.std_key { + println!( + "{}", + print_tag_item(tidx + 1, &format!("{:?}", std_key), &tag.value, 21) + ); + } else { + println!("{}", print_tag_item(tidx + 1, &tag.key, &tag.value, 21)); + } + } + } + + // Print any sub-cues. + if !cue.points.is_empty() { + println!("| Sub-Cues:"); + + for (ptidx, pt) in cue.points.iter().enumerate() { + println!( + "| [{:0>2}] Offset: {:?}", + ptidx + 1, + pt.start_offset_ts + ); + + // Start the number of sub-cue tags, but don't print them. + if !pt.tags.is_empty() { + println!( + "| Sub-Tags: {} (not listed)", + pt.tags.len() + ); + } + } + } + } + } +} + +fn print_tags(tags: &[Tag]) { + if !tags.is_empty() { + println!("|"); + println!("| // Tags //"); + + let mut idx = 1; + + // Print tags with a standard tag key first, these are the most common tags. + for tag in tags.iter().filter(|tag| tag.is_known()) { + if let Some(std_key) = tag.std_key { + println!( + "{}", + print_tag_item(idx, &format!("{:?}", std_key), &tag.value, 4) + ); + } + idx += 1; + } + + // Print the remaining tags with keys truncated to 26 characters. + for tag in tags.iter().filter(|tag| !tag.is_known()) { + println!("{}", print_tag_item(idx, &tag.key, &tag.value, 4)); + idx += 1; + } + } +} + +fn print_visuals(visuals: &[Visual]) { + if !visuals.is_empty() { + println!("|"); + println!("| // Visuals //"); + + for (idx, visual) in visuals.iter().enumerate() { + if let Some(usage) = visual.usage { + println!("| [{:0>2}] Usage: {:?}", idx + 1, usage); + println!("| Media Type: {}", visual.media_type); + } else { + println!("| [{:0>2}] Media Type: {}", idx + 1, visual.media_type); + } + if let Some(dimensions) = visual.dimensions { + println!( + "| Dimensions: {} px x {} px", + dimensions.width, dimensions.height + ); + } + if let Some(bpp) = visual.bits_per_pixel { + println!("| Bits/Pixel: {}", bpp); + } + if let Some(ColorMode::Indexed(colors)) = visual.color_mode { + println!("| Palette: {} colors", colors); + } + println!("| Size: {} bytes", visual.data.len()); + + // Print out tags similar to how regular tags are printed. + if !visual.tags.is_empty() { + println!("| Tags:"); + } + + for (tidx, tag) in visual.tags.iter().enumerate() { + if let Some(std_key) = tag.std_key { + println!( + "{}", + print_tag_item(tidx + 1, &format!("{:?}", std_key), &tag.value, 21) + ); + } else { + println!("{}", print_tag_item(tidx + 1, &tag.key, &tag.value, 21)); + } + } + } + } +} + +fn print_tag_item(idx: usize, key: &str, value: &Value, indent: usize) -> String { + let key_str = match key.len() { + 0..=28 => format!("| {:w$}[{:0>2}] {:<28} : ", "", idx, key, w = indent), + _ => format!( + "| {:w$}[{:0>2}] {:.<28} : ", + "", + idx, + key.split_at(26).0, + w = indent + ), + }; + + let line_prefix = format!("\n| {:w$} : ", "", w = indent + 4 + 28 + 1); + let line_wrap_prefix = format!("\n| {:w$} ", "", w = indent + 4 + 28 + 1); + + let mut out = String::new(); + + out.push_str(&key_str); + + for (wrapped, line) in value.to_string().lines().enumerate() { + if wrapped > 0 { + out.push_str(&line_prefix); + } + + let mut chars = line.chars(); + let split = (0..) + .map(|_| chars.by_ref().take(72).collect::()) + .take_while(|s| !s.is_empty()) + .collect::>(); + + out.push_str(&split.join(&line_wrap_prefix)); + } + + out +} + +fn fmt_time(ts: u64, tb: TimeBase) -> String { + let time = tb.calc_time(ts); + + let hours = time.seconds / (60 * 60); + let mins = (time.seconds % (60 * 60)) / 60; + let secs = f64::from((time.seconds % 60) as u32) + time.frac; + + format!("{}:{:0>2}:{:0>6.3}", hours, mins, secs) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..95fb054 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod addon; +pub mod audio_backend; +pub mod config; +pub mod convert; +pub mod decoder; +pub mod dither; +pub mod metadata; +pub mod player; + +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; +pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1aa0cd4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,109 @@ +use std::fs::File; + +use clap::Command; +use music_player::{ + audio_backend::{self, rodio::RodioSink}, + config::AudioFormat, + convert::Converter, + decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder}, + dither::{mk_ditherer, TriangularDitherer}, +}; +use std::path::Path; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::probe::Hint; +mod formatter; +mod output; + +use log::error; + +type Decoder = Box; + +fn cli() -> Command<'static> { + const VERSION: &str = env!("CARGO_PKG_VERSION"); + Command::new("music-player") + .version(VERSION) + .author("Tsiry Sandratraina ") + .about("A simple music player written in Rust") + .subcommand( + Command::new("play") + .about("Play a song") + .arg_from_usage(" 'The path to the song'"), + ) +} + +#[tokio::main] +async fn main() { + let matches = cli().get_matches(); + + if let Some(matches) = matches.subcommand_matches("play") { + let song = matches.value_of("song").unwrap(); + + formatter::print_format(song); + + let audio_format = AudioFormat::default(); + let backend = audio_backend::find(Some(RodioSink::NAME.to_string())).unwrap(); + + // Create a hint to help the format registry guess what format reader is appropriate. + let mut hint = Hint::new(); + + let path = Path::new(song); + + // Provide the file extension as a hint. + if let Some(extension) = path.extension() { + if let Some(extension_str) = extension.to_str() { + hint.with_extension(extension_str); + } + } + + let source = Box::new(File::open(path).unwrap()); + + // Create the media source stream using the boxed media source from above. + let mss = MediaSourceStream::new(source, Default::default()); + + let symphonia_decoder = |mss: MediaSourceStream, hint| { + SymphoniaDecoder::new(mss, hint).map(|mut decoder| { + // For formats other that Vorbis, we'll try getting normalisation data from + // ReplayGain metadata fields, if present. + Box::new(decoder) as Decoder + }) + }; + + let decoder_type = symphonia_decoder(mss, hint); + + let mut decoder = match decoder_type { + Ok(decoder) => decoder, + Err(e) => { + error!("Failed to create decoder: {}", e); + return; + } + }; + + let mut sink = backend(None, audio_format); + + sink.start(); + + loop { + match decoder.next_packet() { + Ok(result) => { + if let Some((ref _packet_position, packet, channels, sample_rate)) = result { + match packet.samples() { + Ok(_) => { + // println!("packet_position: {:?}", packet_position); + // println!("packet: {:?}", packet.samples()); + let mut converter = + Converter::new(Some(mk_ditherer::)); + sink.write(packet, channels, sample_rate, &mut converter); + } + Err(e) => { + error!("Failed to decode packet: {}", e); + } + } + } + } + Err(e) => { + error!("Failed to decode packet: {}", e); + } + }; + } + } +} diff --git a/src/metadata/audio/file.rs b/src/metadata/audio/file.rs new file mode 100644 index 0000000..be77056 --- /dev/null +++ b/src/metadata/audio/file.rs @@ -0,0 +1 @@ +pub use librespot_protocol::metadata::AudioFile_Format as AudioFileFormat; diff --git a/src/metadata/audio/mod.rs b/src/metadata/audio/mod.rs new file mode 100644 index 0000000..596778e --- /dev/null +++ b/src/metadata/audio/mod.rs @@ -0,0 +1,3 @@ +mod file; + +pub use file::AudioFileFormat; diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs new file mode 100644 index 0000000..fa3e4e4 --- /dev/null +++ b/src/metadata/mod.rs @@ -0,0 +1 @@ +pub mod audio; diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..fe6d944 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,180 @@ +use std::result; + +use symphonia::core::audio::{AudioBufferRef, SignalSpec}; +use symphonia::core::units::Duration; + +pub trait AudioOutput { + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()>; + fn flush(&mut self); +} + +#[allow(dead_code)] +#[allow(clippy::enum_variant_names)] +#[derive(Debug)] +pub enum AudioOutputError { + OpenStreamError, + PlayStreamError, + StreamClosedError, +} + +pub type Result = result::Result; + +mod cpal { + use super::{AudioOutput, AudioOutputError, Result}; + + use symphonia::core::audio::{AudioBufferRef, RawSample, SampleBuffer, SignalSpec}; + use symphonia::core::conv::ConvertibleSample; + use symphonia::core::units::Duration; + + use cpal; + use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + use rb::*; + + use log::error; + + pub struct CpalAudioOutput; + + trait AudioOutputSample: + cpal::Sample + ConvertibleSample + RawSample + std::marker::Send + 'static + { + } + + impl AudioOutputSample for f32 {} + impl AudioOutputSample for i16 {} + impl AudioOutputSample for u16 {} + + impl CpalAudioOutput { + pub fn try_open(spec: SignalSpec, duration: Duration) -> Result> { + // Get default host. + let host = cpal::default_host(); + + // Get the default audio output device. + let device = match host.default_output_device() { + Some(device) => device, + _ => { + error!("failed to get default audio output device"); + return Err(AudioOutputError::OpenStreamError); + } + }; + + let config = match device.default_output_config() { + Ok(config) => config, + Err(err) => { + error!("failed to get default audio output device config: {}", err); + return Err(AudioOutputError::OpenStreamError); + } + }; + + // Select proper playback routine based on sample format. + match config.sample_format() { + cpal::SampleFormat::F32 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device) + } + cpal::SampleFormat::I16 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device) + } + cpal::SampleFormat::U16 => { + CpalAudioOutputImpl::::try_open(spec, duration, &device) + } + } + } + } + + struct CpalAudioOutputImpl + where + T: AudioOutputSample, + { + ring_buf_producer: rb::Producer, + sample_buf: SampleBuffer, + stream: cpal::Stream, + } + + impl CpalAudioOutputImpl { + pub fn try_open( + spec: SignalSpec, + duration: Duration, + device: &cpal::Device, + ) -> Result> { + let num_channels = spec.channels.count(); + + // Output audio stream config. + let config = cpal::StreamConfig { + channels: num_channels as cpal::ChannelCount, + sample_rate: cpal::SampleRate(spec.rate), + buffer_size: cpal::BufferSize::Default, + }; + + // Create a ring buffer with a capacity for up-to 200ms of audio. + let ring_len = ((200 * spec.rate as usize) / 1000) * num_channels; + + let ring_buf = SpscRb::new(ring_len); + let (ring_buf_producer, ring_buf_consumer) = (ring_buf.producer(), ring_buf.consumer()); + + let stream_result = device.build_output_stream( + &config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + // Write out as many samples as possible from the ring buffer to the audio + // output. + let written = ring_buf_consumer.read(data).unwrap_or(0); + // Mute any remaining samples. + data[written..].iter_mut().for_each(|s| *s = T::MID); + }, + move |err| error!("audio output error: {}", err), + ); + + if let Err(err) = stream_result { + error!("audio output stream open error: {}", err); + + return Err(AudioOutputError::OpenStreamError); + } + + let stream = stream_result.unwrap(); + + // Start the output stream. + if let Err(err) = stream.play() { + error!("audio output stream play error: {}", err); + + return Err(AudioOutputError::PlayStreamError); + } + + let sample_buf = SampleBuffer::::new(duration, spec); + + Ok(Box::new(CpalAudioOutputImpl { + ring_buf_producer, + sample_buf, + stream, + })) + } + } + + impl AudioOutput for CpalAudioOutputImpl { + fn write(&mut self, decoded: AudioBufferRef<'_>) -> Result<()> { + // Do nothing if there are no audio frames. + if decoded.frames() == 0 { + return Ok(()); + } + + // Audio samples must be interleaved for cpal. Interleave the samples in the audio + // buffer into the sample buffer. + self.sample_buf.copy_interleaved_ref(decoded); + + // Write all the interleaved samples to the ring buffer. + let mut samples = self.sample_buf.samples(); + + while let Some(written) = self.ring_buf_producer.write_blocking(samples) { + samples = &samples[written..]; + } + + Ok(()) + } + + fn flush(&mut self) { + // Flush is best-effort, ignore the returned result. + let _ = self.stream.pause(); + } + } +} + +pub fn try_open(spec: SignalSpec, duration: Duration) -> Result> { + cpal::CpalAudioOutput::try_open(spec, duration) +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..575a866 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,469 @@ +use futures_util::{future::FusedFuture, Future}; +use log::{error, trace, warn}; +use parking_lot::Mutex; +use std::{ + collections::HashMap, + fs::File, + mem, + path::Path, + pin::Pin, + process::exit, + sync::Arc, + task::{Context, Poll}, + thread, +}; +use symphonia::core::{ + codecs::{DecoderOptions, CODEC_TYPE_NULL}, + errors::Error, + formats::FormatOptions, + io::MediaSourceStream, + meta::MetadataOptions, + probe::Hint, +}; +use tokio::sync::mpsc; + +use crate::{ + audio_backend::Sink, + decoder::{symphonia_decoder::SymphoniaDecoder, AudioDecoder}, + metadata::audio::AudioFileFormat, +}; + +const PRELOAD_NEXT_TRACK_BEFORE_END: u64 = 30000; + +pub type PlayerResult = Result<(), Error>; + +pub struct Player { + commands: Option>, + thread_handle: Option>, +} + +impl Player { + pub fn new(sink_builder: F) -> (Player, PlayerEventChannel) + where + F: FnOnce() -> Box + Send + 'static, + { + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); + let (event_sender, event_receiver) = mpsc::unbounded_channel(); + + let handle = thread::spawn(move || { + let internal = PlayerInternal { + commands: cmd_rx, + load_handles: Arc::new(Mutex::new(HashMap::new())), + sink: sink_builder(), + state: PlayerState::Stopped, + preload: PlayerPreload::None, + sink_status: SinkStatus::Closed, + sink_event_callback: None, + event_senders: [event_sender].to_vec(), + }; + let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); + runtime.block_on(internal); + }); + ( + Player { + commands: Some(cmd_tx), + thread_handle: Some(handle), + }, + event_receiver, + ) + } + + fn command(&self, cmd: PlayerCommand) { + if let Some(commands) = self.commands.as_ref() { + if let Err(e) = commands.send(cmd) { + error!("Player Commands Error: {}", e); + } + } + } + + pub fn load(&mut self, track_id: &str, start_playing: bool, position_ms: u32) { + self.command(PlayerCommand::Load { + track_id: track_id.to_string(), + }); + } + + pub fn preload(&self, track_id: &str) { + self.command(PlayerCommand::Preload); + } + + pub fn play(&self) { + self.command(PlayerCommand::Play) + } + + pub fn pause(&self) { + self.command(PlayerCommand::Pause) + } + + pub fn stop(&self) { + self.command(PlayerCommand::Stop) + } + + pub fn seek(&self, position_ms: u32) { + self.command(PlayerCommand::Seek(position_ms)); + } + + pub fn get_player_event_channel(&self) -> PlayerEventChannel { + let (event_sender, event_receiver) = mpsc::unbounded_channel(); + self.command(PlayerCommand::AddEventSender(event_sender)); + event_receiver + } + + pub async fn await_end_of_track(&self) { + let mut channel = self.get_player_event_channel(); + while let Some(event) = channel.recv().await { + if matches!( + event, + PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. } + ) { + return; + } + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum SinkStatus { + Running, + Closed, + TemporarilyClosed, +} + +pub type SinkEventCallback = Box; + +struct PlayerInternal { + commands: mpsc::UnboundedReceiver, + load_handles: Arc>>>, + + state: PlayerState, + preload: PlayerPreload, + sink: Box, + sink_status: SinkStatus, + sink_event_callback: Option, + event_senders: Vec>, +} + +impl Future for PlayerInternal { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + loop { + let mut all_futures_completed_or_not_ready = true; + + // process commands that were sent to us + let cmd = match self.commands.poll_recv(cx) { + Poll::Ready(None) => return Poll::Ready(()), // client has disconnected - shut down. + Poll::Ready(Some(cmd)) => { + all_futures_completed_or_not_ready = false; + Some(cmd) + } + _ => None, + }; + + if let Some(cmd) = cmd { + if let Err(e) = self.handle_command(cmd) { + // error!("Error handling command: {}", e); + } + } + } + } +} + +impl PlayerInternal { + fn ensure_sink_running(&mut self) { + if self.sink_status != SinkStatus::Running { + trace!("== Starting sink =="); + if let Some(callback) = &mut self.sink_event_callback { + callback(SinkStatus::Running); + } + match self.sink.start() { + Ok(()) => self.sink_status = SinkStatus::Running, + Err(e) => { + error!("{}", e); + exit(1); + } + } + } + } + + fn ensure_sink_stopped(&mut self, temporarily: bool) { + match self.sink_status { + SinkStatus::Running => { + trace!("== Stopping sink =="); + match self.sink.stop() { + Ok(()) => { + self.sink_status = if temporarily { + SinkStatus::TemporarilyClosed + } else { + SinkStatus::Closed + }; + if let Some(callback) = &mut self.sink_event_callback { + callback(self.sink_status); + } + } + Err(e) => { + error!("{}", e); + exit(1); + } + } + } + SinkStatus::TemporarilyClosed => { + if !temporarily { + self.sink_status = SinkStatus::Closed; + if let Some(callback) = &mut self.sink_event_callback { + callback(SinkStatus::Closed); + } + } + } + SinkStatus::Closed => (), + } + } + + fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult { + match cmd { + PlayerCommand::Load { track_id } => { + self.handle_command_load(&track_id); + } + PlayerCommand::Preload => { + self.handle_command_preload(); + } + PlayerCommand::Play => { + self.handle_play(); + } + PlayerCommand::Pause => { + self.handle_pause(); + } + PlayerCommand::Stop => { + self.handle_player_stop(); + } + PlayerCommand::Seek(position_ms) => { + self.handle_command_seek(); + } + PlayerCommand::SetSinkEventCallback => { + self.sink_event_callback = None; + } + PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender), + } + Ok(()) + } + + async fn load_track(&self, song: &str) -> Option { + // Create a hint to help the format registry guess what format reader is appropriate. + let mut hint = Hint::new(); + + let path = Path::new(song); + + // Provide the file extension as a hint. + if let Some(extension) = path.extension() { + if let Some(extension_str) = extension.to_str() { + hint.with_extension(extension_str); + } + } + + let source = Box::new(File::open(path).unwrap()); + + // Create the media source stream using the boxed media source from above. + let mss = MediaSourceStream::new(source, Default::default()); + + let symphonia_decoder = |mss: MediaSourceStream, hint| { + SymphoniaDecoder::new(mss, hint).map(|mut decoder| { + // For formats other that Vorbis, we'll try getting normalisation data from + // ReplayGain metadata fields, if present. + Box::new(decoder) as Decoder + }) + }; + + let decoder_type = symphonia_decoder(mss, hint); + + let mut decoder = match decoder_type { + Ok(decoder) => decoder, + Err(e) => { + panic!("Failed to create decoder: {}", e); + } + }; + return Some(PlayerLoadedTrackData { + decoder, + bytes_per_second: 0, + duration_ms: 0, + stream_position_ms: 0, + is_explicit: false, + }); + } + + fn start_playback(&mut self, track_id: &str, loaded_track: PlayerLoadedTrackData) { + self.ensure_sink_running(); + self.send_event(PlayerEvent::Playing { + track_id: track_id.to_string(), + }); + + self.state = PlayerState::Playing { + track_id: track_id.to_string(), + decoder: loaded_track.decoder, + }; + } + + fn send_event(&mut self, event: PlayerEvent) { + self.event_senders + .retain(|sender| sender.send(event.clone()).is_ok()); + } + + fn handle_command_load(&mut self, track_id: &str) { + println!("load track {}", track_id); + self.load_track(track_id); + + // + // + } + + fn handle_command_preload(&self) { + todo!() + } + + fn handle_play(&self) { + todo!() + } + + fn handle_player_stop(&self) { + todo!() + } + + fn handle_pause(&self) { + todo!() + } + + fn handle_command_seek(&self) { + todo!() + } +} + +struct PlayerLoadedTrackData { + decoder: Decoder, + bytes_per_second: usize, + duration_ms: u32, + stream_position_ms: u32, + is_explicit: bool, +} + +enum PlayerPreload { + None, + Loading { + loader: Pin> + Send>>, + track_id: String, + }, + Ready { + loaded_track: Box, + }, +} + +type Decoder = Box; + +enum PlayerState { + Stopped, + Loading { + track_id: String, + loader: Pin> + Send>>, + }, + Paused { + decoder: Decoder, + }, + Playing { + decoder: Decoder, + track_id: String, + }, + EndOfTrack { + loaded_track: PlayerLoadedTrackData, + }, + Invalid, +} + +impl PlayerState { + fn is_playing(&self) -> bool { + use self::PlayerState::*; + match *self { + Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false, + Playing { .. } => true, + Invalid => { + // "PlayerState::is_playing in invalid state" + exit(1); + } + } + } + + #[allow(dead_code)] + fn is_stopped(&self) -> bool { + use self::PlayerState::*; + matches!(self, Stopped) + } + + #[allow(dead_code)] + fn is_loading(&self) -> bool { + use self::PlayerState::*; + matches!(self, Loading { .. }) + } + + fn decoder(&mut self) -> Option<&mut Decoder> { + use self::PlayerState::*; + match *self { + Stopped | EndOfTrack { .. } | Loading { .. } => None, + Paused { + ref mut decoder, .. + } + | Playing { + ref mut decoder, .. + } => Some(decoder), + Invalid => { + // error!("PlayerState::decoder in invalid state"); + exit(1); + } + } + } +} + +pub struct PlayerTrackLoader {} + +impl PlayerTrackLoader { + fn stream_data_rate(&self, format: AudioFileFormat) -> usize { + let kbps = match format { + AudioFileFormat::OGG_VORBIS_96 => 12, + AudioFileFormat::OGG_VORBIS_160 => 20, + AudioFileFormat::OGG_VORBIS_320 => 40, + AudioFileFormat::MP3_256 => 32, + AudioFileFormat::MP3_320 => 40, + AudioFileFormat::MP3_160 => 20, + AudioFileFormat::MP3_96 => 12, + AudioFileFormat::MP3_160_ENC => 20, + AudioFileFormat::MP4_128_DUAL => todo!(), + AudioFileFormat::OTHER3 => todo!(), + AudioFileFormat::AAC_160 => todo!(), + AudioFileFormat::AAC_320 => todo!(), + AudioFileFormat::MP4_128 => todo!(), + AudioFileFormat::OTHER5 => todo!(), + }; + kbps * 1024 + } +} + +enum PlayerCommand { + Load { track_id: String }, + Preload, + Play, + Pause, + Stop, + Seek(u32), + AddEventSender(mpsc::UnboundedSender), + SetSinkEventCallback, +} + +#[derive(Debug, Clone)] +pub enum PlayerEvent { + Stopped, + Started, + Loading { track_id: String }, + Preloading, + Playing { track_id: String }, + Paused, + TimeToPreloadNextTrack, + EndOfTrack, + VolumeSet { volume: u16 }, +} + +pub type PlayerEventChannel = mpsc::UnboundedReceiver;