diff --git a/Cargo.lock b/Cargo.lock index 50abf13..871c1ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -292,6 +314,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -529,6 +560,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -619,6 +656,20 @@ dependencies = [ "libm", ] +[[package]] +name = "coreaudio-rs" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" +dependencies = [ + "bitflags 2.10.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "cosmic-text" version = "0.15.0" @@ -643,6 +694,36 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.3", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -677,6 +758,15 @@ dependencies = [ "wgpu", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor-lite" version = "0.1.1" @@ -689,6 +779,23 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -865,6 +972,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1373,6 +1486,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -1529,6 +1651,7 @@ dependencies = [ "iced_core", "log", "rustc-hash 2.1.1", + "tokio", "wasm-bindgen-futures", "wasmtimer", ] @@ -1807,13 +1930,17 @@ version = "0.1.1" dependencies = [ "chrono", "dirs", + "hex", "iced", "rand", "reqwest", "rhai", + "rodio", "rusqlite", "serde", "serde_json", + "sha2", + "tokio", ] [[package]] @@ -1885,6 +2012,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1994,6 +2127,15 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2157,6 +2299,47 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2270,6 +2453,31 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.10.0", + "libc", + "objc2 0.6.3", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2305,6 +2513,29 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.3", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -2335,7 +2566,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", ] @@ -2864,6 +3097,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -3022,6 +3265,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rodio" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb" +dependencies = [ + "cpal", + "dasp_sample", + "num-rational", + "rand", + "rand_distr", + "rtrb", + "symphonia", + "thiserror 2.0.18", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -3038,6 +3297,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "rtrb" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" + [[package]] name = "rusqlite" version = "0.39.0" @@ -3281,6 +3546,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3514,6 +3790,153 @@ dependencies = [ "zeno", ] +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "2.0.114" @@ -3726,9 +4149,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -3884,6 +4319,12 @@ dependencies = [ "core_maths", ] +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 33c4925..33c8c8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.1" edition = "2024" [dependencies] -iced = { version = "0.14.0"} +iced = { version = "0.14.0", features = ["tokio"]} rand = "0.10.0" dirs = "6.0.0" serde = { version = "1.0.144", features = ["derive"] } @@ -13,4 +13,8 @@ rhai = "1.24.0" chrono = "0.4.44" rusqlite = { version = "0.39.0", features = ["chrono"] } reqwest = { version = "0.12", features = ["json", "stream"] } +rodio = "0.22.2" +sha2 = "0.11.0" +hex = "0.4.3" +tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index e681312..a5b57ca 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod words; pub(crate) mod card_sets; pub(crate) mod card_stats; +pub(crate) mod voice; diff --git a/src/data_provider/voice.rs b/src/data_provider/voice.rs new file mode 100644 index 0000000..89ad580 --- /dev/null +++ b/src/data_provider/voice.rs @@ -0,0 +1,39 @@ +use crate::dictionary::app_data_dir; +use rodio::Decoder; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::BufReader; +use reqwest::Client; + +pub async fn get_voice(text: &str) -> BufReader { + let mut path = app_data_dir(); + path.push("voice"); + if !path.exists() { + std::fs::create_dir(path.clone()).unwrap(); + } + let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes()))); + path.push(hash); + if !path.exists() { + let engine_url = "http://127.0.0.1:50021"; + let client = Client::new(); + let query = client + .post(format!("{engine_url}/audio_query?text={text}&speaker=11")) + .send() + .await; + + let query = query.unwrap().text().await.unwrap(); + // Synthesis + let audio = client + .post(format!("{engine_url}/synthesis?speaker=11")) + .header("Content-Type", "application/json") + .body(query) + .send() + .await.unwrap() + .bytes() + .await.unwrap(); + + tokio::fs::write(&path, &audio).await.unwrap(); + } + + BufReader::new(File::open(path).unwrap()) +} diff --git a/src/repetition.rs b/src/repetition.rs index df8f20f..9979c82 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,3 +1,4 @@ +use crate::data_provider::voice::get_voice; use crate::lang::{CardSet, DictionaryElement, WordOpenMode}; use crate::repetitions::CardSetSettings; use crate::Page::PreviousPage; @@ -6,7 +7,9 @@ use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; use iced::widget::{button, column, container, row, rule, space, text}; use iced::{alignment, keyboard, Element, Fill, Left, Task}; +use rodio::MixerDeviceSink; use std::sync::{Arc, Mutex}; +use tokio::task::spawn_blocking; pub struct RepetitionState { pub settings: CardSetSettings, @@ -14,6 +17,8 @@ pub struct RepetitionState { pub state: Arc>, current_word: DictionaryElement, open: bool, + can_play: bool, + sink: Arc, } impl NavigatedPage for RepetitionState { @@ -30,12 +35,16 @@ impl RepetitionState { pub(crate) fn new(set: CardSetSettings, state: Arc>) -> RepetitionState { let mut card_set = CardSet::new(&set, state.clone()); let word = card_set.next(); + let sink_handle = rodio::DeviceSinkBuilder::open_default_sink().unwrap(); + RepetitionState { settings: set, set: card_set, state, current_word: word, open: false, + can_play: true, + sink: Arc::new(sink_handle) } } } @@ -46,11 +55,26 @@ impl RepetitionState { RepetitionMessage::Back => {} RepetitionMessage::Next => self.next(), RepetitionMessage::Answer(m) => self.answer(m), + RepetitionMessage::Play => { + if !self.can_play { + return Task::none() + } + + self.can_play = false; + let value = self.current_word.key.clone(); + + return Task::perform(play_sound(self.sink.clone(), value), |_| RootMessage::Repetition(RepetitionMessage::PlayFinished)); + }, + RepetitionMessage::PlayFinished => { + self.can_play = true; + } } Task::none() } + + fn next(&mut self) { if self.open { self.set.open(WordOpenMode::None); @@ -109,6 +133,7 @@ impl RepetitionState { match self.settings.forward.as_str() { "key" => self.draw_key(word), "value" => self.draw_value(word), + "speech" => self.draw_voice(word), _ => space().into(), } } @@ -122,6 +147,7 @@ impl RepetitionState { match self.settings.backward.as_str() { "key" => self.draw_key(word), "value" => self.draw_value(word), + "speech" => self.draw_voice(word), _ => space().into(), } } @@ -147,6 +173,12 @@ impl RepetitionState { fn draw_value(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { text!("{}", word.value).size(24).into() } + + fn draw_voice(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { + button("Воспроизвести") + .on_press(RepetitionMessage::Play) + .into() + } } impl KeyPressedPage for RepetitionState { @@ -180,4 +212,14 @@ pub enum RepetitionMessage { Next, Back, Answer(WordOpenMode), + Play, + PlayFinished, } + +async fn play_sound(sink: Arc, text: String) { + let data = get_voice(text.as_str()).await; + spawn_blocking(move || { + rodio::play(&sink.mixer(), data).unwrap().sleep_until_end(); + + }).await.unwrap(); +} \ No newline at end of file