From 2255479ec395e1edd5b68f23dbc8da481c4f4b65 Mon Sep 17 00:00:00 2001 From: Abraham Date: Sun, 30 Apr 2023 22:32:48 +0500 Subject: [PATCH] Recorder rewritten. + Attempt to integrate cpal and portaudio. --- package.json | 2 +- src-tauri/Cargo.lock | 124 +++++-- src-tauri/Cargo.toml | 5 +- src-tauri/rustpotter/___default.rpw | Bin 0 -> 9182 bytes src-tauri/rustpotter/jarvis-community-1.rpw | Bin 0 -> 9273 bytes src-tauri/rustpotter/jarvis-community-2.rpw | Bin 0 -> 8044 bytes src-tauri/rustpotter/jarvis-community-3.rpw | Bin 0 -> 7890 bytes src-tauri/rustpotter/jarvis-community-4.rpw | Bin 0 -> 8272 bytes src-tauri/rustpotter/jarvis-community-5.rpw | Bin 0 -> 7685 bytes src-tauri/rustpotter/jarvis-default.rpw | Bin 0 -> 9116 bytes src-tauri/src/.cargo/config.toml | 2 + src-tauri/src/assistant_commands.rs | 13 +- src-tauri/src/config.rs | 18 +- src-tauri/src/main.rs | 16 +- src-tauri/src/recorder.rs | 139 +++++-- src-tauri/src/recorder/cpal.rs | 186 ++++++++++ src-tauri/src/recorder/portaudio.rs | 201 +++++++++++ src-tauri/src/recorder/pvrecorder.rs | 105 ++++++ src-tauri/src/tauri_commands.rs | 4 +- .../tauri_commands/{recorder.rs => audio.rs} | 0 src-tauri/src/tauri_commands/listener.rs | 340 ++++++++++++------ src-tauri/tauri.conf.json | 3 +- src/pages/settings.svelte | 8 +- 23 files changed, 983 insertions(+), 183 deletions(-) create mode 100644 src-tauri/rustpotter/___default.rpw create mode 100644 src-tauri/rustpotter/jarvis-community-1.rpw create mode 100644 src-tauri/rustpotter/jarvis-community-2.rpw create mode 100644 src-tauri/rustpotter/jarvis-community-3.rpw create mode 100644 src-tauri/rustpotter/jarvis-community-4.rpw create mode 100644 src-tauri/rustpotter/jarvis-community-5.rpw create mode 100644 src-tauri/rustpotter/jarvis-default.rpw create mode 100644 src-tauri/src/.cargo/config.toml create mode 100644 src-tauri/src/recorder/cpal.rs create mode 100644 src-tauri/src/recorder/portaudio.rs create mode 100644 src-tauri/src/recorder/pvrecorder.rs rename src-tauri/src/tauri_commands/{recorder.rs => audio.rs} (100%) diff --git a/package.json b/package.json index f993033..cc3adcf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jarvis-app", "private": true, - "version": "0.0.1", + "version": "0.0.2", "type": "module", "scripts": { "dev": "routify -c dev:vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 25a3cf1..feedbde 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -48,7 +48,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" dependencies = [ "alsa-sys", - "bitflags", + "bitflags 1.3.2", "libc", "nix", ] @@ -69,6 +69,12 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "arrayvec" version = "0.7.2" @@ -82,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "glib", "libc", ] @@ -99,6 +105,17 @@ dependencies = [ "system-deps 6.0.5", ] +[[package]] +name = "atomic_enum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -123,7 +140,7 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -137,6 +154,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bitflags" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" + [[package]] name = "bitflags" version = "1.3.2" @@ -225,7 +248,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -362,7 +385,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", @@ -378,7 +401,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", @@ -437,7 +460,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -450,7 +473,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "foreign-types", "libc", @@ -462,7 +485,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation-sys 0.6.2", "coreaudio-sys", ] @@ -901,7 +924,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk-sys", @@ -917,7 +940,7 @@ version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -1018,7 +1041,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -1048,7 +1071,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -1124,7 +1147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" dependencies = [ "atk", - "bitflags", + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1361,14 +1384,17 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jarvis-app" -version = "0.0.1" +version = "0.0.2" dependencies = [ + "arc-swap", + "atomic_enum", "hound", "lazy_static", "log", "once_cell", "peak_alloc", "pickledb", + "portaudio", "pv_porcupine", "pv_recorder", "rand 0.8.5", @@ -1391,7 +1417,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "javascriptcore-rs-sys", ] @@ -1670,7 +1696,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys 0.3.0", "num_enum", @@ -1683,7 +1709,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys 0.4.1+23.1.7779620", "num_enum", @@ -1727,7 +1753,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", ] @@ -1758,6 +1784,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-integer", + "num-iter", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.3" @@ -1788,6 +1825,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -1938,7 +1986,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "libc", "once_cell", @@ -2156,13 +2204,25 @@ version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide 0.7.1", ] +[[package]] +name = "portaudio" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d289315f6155a7608b6d8757786c79ed2243afeab8a5eda8989effda3fdc5c3" +dependencies = [ + "bitflags 0.7.0", + "libc", + "num", + "pkg-config", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2379,7 +2439,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2388,7 +2448,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2519,7 +2579,7 @@ version = "0.37.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -2585,7 +2645,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", @@ -2802,7 +2862,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio", "glib", "libc", @@ -2816,7 +2876,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio-sys", "glib-sys", "gobject-sys", @@ -2895,7 +2955,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55a0846e7a2c9a8081ff799fc83a975170417ad2a143f644a77ec2e3e82a2b73" dependencies = [ - "bitflags", + "bitflags 1.3.2", "lazy_static", "log", "symphonia-core", @@ -2909,7 +2969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9567e2d8a5f866b2f94f5d366d811e0c6826babcff6d37de9e1a6690d38869" dependencies = [ "arrayvec", - "bitflags", + "bitflags 1.3.2", "bytemuck", "lazy_static", "log", @@ -2995,7 +3055,7 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac8e6399427c8494f9849b58694754d7cc741293348a6836b6c8d2c5aa82d8e6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "cc", "cocoa", @@ -3691,7 +3751,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk", "gdk-sys", @@ -3716,7 +3776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3bf2065..d3dfc63 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jarvis-app" -version = "0.0.1" +version = "0.0.2" description = "Jarvis Voice Assistant" authors = ["Abraham Tugalov"] license = "GPL-3.0-only" @@ -36,6 +36,9 @@ rustpotter = "2.0.0" simple-logging = "2.0.2" log = "0.4.17" once_cell = "1.17.1" +arc-swap = "1.6.0" +atomic_enum = "0.2.0" +portaudio = "0.7.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/rustpotter/___default.rpw b/src-tauri/rustpotter/___default.rpw new file mode 100644 index 0000000000000000000000000000000000000000..79986942d6406d8d1d32750f2a5f7585520a37fb GIT binary patch literal 9182 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 literal 0 HcmV?d00001 diff --git a/src-tauri/rustpotter/jarvis-community-1.rpw b/src-tauri/rustpotter/jarvis-community-1.rpw new file mode 100644 index 0000000000000000000000000000000000000000..07874a56c187b79069384bba71b678179a29da39 GIT binary patch literal 9273 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 literal 0 HcmV?d00001 diff --git a/src-tauri/rustpotter/jarvis-community-2.rpw b/src-tauri/rustpotter/jarvis-community-2.rpw new file mode 100644 index 0000000000000000000000000000000000000000..be4df0b85ca5d368d8b441ef02facf06ebe8dff7 GIT binary patch literal 8044 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 literal 0 HcmV?d00001 diff --git a/src-tauri/rustpotter/jarvis-community-4.rpw b/src-tauri/rustpotter/jarvis-community-4.rpw new file mode 100644 index 0000000000000000000000000000000000000000..0a0341a528997ee010b67b10d146866664cff0d5 GIT binary patch literal 8272 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 CsC4Q8 literal 0 HcmV?d00001 diff --git a/src-tauri/rustpotter/jarvis-community-5.rpw b/src-tauri/rustpotter/jarvis-community-5.rpw new file mode 100644 index 0000000000000000000000000000000000000000..e5d2a1f3d0b4f011b0323fbdceb28e0d29660863 GIT binary patch literal 7685 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 BEE)g+ literal 0 HcmV?d00001 diff --git a/src-tauri/rustpotter/jarvis-default.rpw b/src-tauri/rustpotter/jarvis-default.rpw new file mode 100644 index 0000000000000000000000000000000000000000..47dea84064a932d285e50546cca2c612361b7c5d GIT binary patch literal 9116 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 Result, String> { if let Ok(parse_result) = serde_yaml::from_reader::(cc_reader) { cc_yaml = parse_result; } else { - println!("Can't parse {}, skipping ...", &cc_file.display()); + warn!("Can't parse {}, skipping ...", &cc_file.display()); continue; // return Err(format!("Can't parse {}", &cc_file.display())); } @@ -52,9 +53,11 @@ pub fn parse_commands() -> Result, String> { if commands.len() > 0 { Ok(commands) } else { + error!("No commands were found"); Err("No commands were found".into()) } } else { + error!("Error reading commands directory"); return Err("Error reading commands directory".into()); } } @@ -95,6 +98,7 @@ pub fn fetch_command<'a>( if let Some((cmd_path, scmd)) = result_scmd { println!("Ratio is: {}", current_max_ratio); + info!("CMD is: {cmd_path:?}, SCMD is: {scmd:?}, Ratio is: {}", current_max_ratio); Some((&cmd_path, &scmd)) } else { None @@ -144,6 +148,7 @@ pub fn execute_command( Ok(()) } else { + error!("AHK process spawn error (does exe path is valid?)"); Err("AHK process spawn error (does exe path is valid?)".into()) } } @@ -169,6 +174,7 @@ pub fn execute_command( Ok(()) } else { + error!("Shell process spawn error (does cli command is valid?)"); Err("Shell process spawn error (does cli command is valid?)".into()) } } @@ -184,6 +190,9 @@ pub fn execute_command( std::thread::sleep(Duration::from_secs(2)); std::process::exit(0); } - _ => Err("Command type unknown".into()), + _ => { + error!("Command type unknown"); + Err("Command type unknown".into()) + }, } } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 3ecb824..f50bef8 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -7,7 +7,15 @@ // "./public" // }; -pub const WAKE_WORD_ENGINES: [&str; 2] = ["rustpotter", "picovoice"]; +// APP +// pub const WAKE_WORD_ENGINES: [&str; 3] = ["rustpotter", "vosk", "picovoice"]; +pub enum WakeWordEngine { + Rustpotter, + Vosk, + Porcupine +} + +pub const DEFAULT_WAKE_WORD_ENGINE: WakeWordEngine = WakeWordEngine::Rustpotter; pub const DB_FILE_NAME: &str = "app.db"; pub const LOG_FILE_NAME: &str = "log.txt"; @@ -15,12 +23,20 @@ pub const APP_VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS"); pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY"); +// RUSPOTTER +pub const RUSPOTTER_MIN_SCORE: f32 = 0.64; + +// PICOVOICE pub const COMMANDS_PATH: &str = "commands/"; pub const KEYWORDS_PATH: &str = "picovoice/keywords/"; +// VOSK // pub const VOSK_MODEL_PATH: &str = const_concat!(PUBLIC_PATH, "/vosk/model_small"); +pub const VOSK_FETCH_PHRASE: &str = "джарвис"; pub const VOSK_MODEL_PATH: &str = "vosk/model_small"; +pub const VOSK_MIN_RATIO: f64 = 70.0; +// ETC pub const CMD_RATIO_THRESHOLD: f64 = 60f64; pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(10); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4f66a52..80d5fe9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -33,6 +33,11 @@ lazy_static! { static ref APP_CONFIG_DIR: Mutex = Mutex::new(String::new()); } +// data dir +lazy_static! { + static ref APP_LOG_DIR: Mutex = Mutex::new(String::new()); +} + // init PickleDb connection lazy_static! { static ref DB: Mutex = Mutex::new( @@ -58,9 +63,6 @@ lazy_static! { } fn main() { - // log to file - simple_logging::log_to_file(config::LOG_FILE_NAME, LevelFilter::max()).expect("Failed to start logger ... is directory writable?"); - // init vosk vosk::init_vosk(); @@ -70,6 +72,14 @@ fn main() { std::fs::create_dir_all(app.path_resolver().app_config_dir().unwrap())?; APP_CONFIG_DIR.lock().unwrap().push_str(app.path_resolver().app_config_dir().unwrap().to_str().unwrap()); + std::fs::create_dir_all(app.path_resolver().app_log_dir().unwrap())?; + APP_LOG_DIR.lock().unwrap().push_str(app.path_resolver().app_log_dir().unwrap().to_str().unwrap()); + + // log to file + let log_file_path = format!("{}/{}", APP_LOG_DIR.lock().unwrap(), config::LOG_FILE_NAME); + println!("!!!===============!!!\nLOGGING TO {}\n!!!===============!!!\n", &log_file_path); + simple_logging::log_to_file(log_file_path, LevelFilter::max()).expect("Failed to start logger ... is directory writable?"); + Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src-tauri/src/recorder.rs b/src-tauri/src/recorder.rs index f8fad40..502bb87 100644 --- a/src-tauri/src/recorder.rs +++ b/src-tauri/src/recorder.rs @@ -1,52 +1,127 @@ -use once_cell::sync::OnceCell; -use std::sync::atomic::{AtomicU32, AtomicBool, Ordering}; -use pv_recorder::{Recorder, RecorderBuilder}; -use log::{info}; +// use once_cell::sync::OnceCell; +use std::sync::atomic::{AtomicU32, Ordering}; +use log::{info, warn, error}; +use atomic_enum::atomic_enum; + +mod pvrecorder; +// mod cpal; +// mod portaudio; use crate::DB; +#[atomic_enum] +#[derive(PartialEq)] +pub enum RecorderType { + Cpal, + PvRecorder, + PortAudio +} + +pub static RECORDER_TYPE: AtomicRecorderType = AtomicRecorderType::new(RecorderType::PvRecorder); // use pvrecorder as default pub static FRAME_LENGTH: AtomicU32 = AtomicU32::new(0); -static RECORDER: OnceCell = OnceCell::new(); -pub static IS_RECORDING: AtomicBool = AtomicBool::new(false); -fn init_microphone() { - if RECORDER.get().is_none() { - RECORDER.get_or_init(|| RecorderBuilder::new() - .device_index(get_selected_microphone_index()) - .frame_length(FRAME_LENGTH.load(Ordering::SeqCst) as i32) - .init() - .expect("Failed to initialize pvrecorder")); - info!("Microphone recorder initialized!") +pub fn init() { + match RECORDER_TYPE.load(Ordering::SeqCst) { + RecorderType::PvRecorder => { + // Init Pv Recorder + info!("Initializing Pv Recorder audio backend."); + match pvrecorder::init_microphone(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)) { + false => { + // Switch to CPAL recorder + warn!("Pv Recorder audio backend failed."); + // RECORDER_TYPE.store(RecorderType::PortAudio, Ordering::SeqCst); + + // init again + init(); + }, + _ => () + } + }, + RecorderType::PortAudio => { + // Init PortAudio + info!("Initializing PortAudio audio backend"); + todo!(); + // match portaudio::init_microphone(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)) { + // false => { + // // Switch to PortAudio recorder + // error!("PortAudio audio backend failed."); + // }, + // _ => () + // } + }, + RecorderType::Cpal => { + // Init CPAL + info!("Initializing CPAL audio backend"); + todo!(); + // match cpal::init_microphone(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)) { + // false => { + // // Switch to CPAL recorder + // error!("CPAL audio backend failed."); + // }, + // _ => () + // } + } } } pub fn read_microphone(frame_buffer: &mut [i16]) { - // ensure microphone is initialized - init_microphone(); - - // read to frame buffer - RECORDER.get().unwrap().read(frame_buffer).expect("Failed to read audio frame"); + match RECORDER_TYPE.load(Ordering::SeqCst) { + RecorderType::PvRecorder => { + pvrecorder::read_microphone(frame_buffer); + }, + RecorderType::PortAudio => { + todo!(); + // portaudio::read_microphone(frame_buffer); + }, + RecorderType::Cpal => { + // cpal::read_microphone(frame_buffer); + panic!("Cpal should be used via callback assignment"); + } + } } pub fn start_recording() { - // ensure microphone is initialized - init_microphone(); - - RECORDER.get().unwrap().start().expect("Failed to start audio recording!"); - IS_RECORDING.store(true, Ordering::SeqCst); - info!("START recording from microphone ..."); + match RECORDER_TYPE.load(Ordering::SeqCst) { + RecorderType::PvRecorder => { + pvrecorder::start_recording(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)); + }, + RecorderType::PortAudio => { + todo!(); + // portaudio::start_recording(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)); + }, + RecorderType::Cpal => { + // cpal::start_recording(get_selected_microphone_index(), FRAME_LENGTH.load(Ordering::SeqCst)); + } + } } pub fn stop_recording() { - // ensure microphone is initialized - init_microphone(); - - RECORDER.get().unwrap().start().expect("Failed to start audio recording!"); - IS_RECORDING.store(false, Ordering::SeqCst); - info!("STOP recording from microphone ..."); + match RECORDER_TYPE.load(Ordering::SeqCst) { + RecorderType::PvRecorder => { + pvrecorder::stop_recording(); + }, + RecorderType::PortAudio => { + todo!(); + // portaudio::stop_recording(); + }, + RecorderType::Cpal => { + // cpal::stop_recording(); + } + } } +// pub fn update_selected_microphone_index() -> i32 { +// let selected_microphone: i32 = get_selected_microphone_index(); + +// // store current microphone idx +// SELECTED_MICROPHONE_IDX.store(selected_microphone, Ordering::SeqCst); + +// // return microphone index +// info!("Selected microphone index = {selected_microphone}"); +// selected_microphone +// } + pub fn get_selected_microphone_index() -> i32 { let selected_microphone: i32; @@ -57,7 +132,5 @@ pub fn get_selected_microphone_index() -> i32 { selected_microphone = -1; } - // return microphone index - info!("Selected microphone index = {selected_microphone}"); selected_microphone } \ No newline at end of file diff --git a/src-tauri/src/recorder/cpal.rs b/src-tauri/src/recorder/cpal.rs new file mode 100644 index 0000000..27840fe --- /dev/null +++ b/src-tauri/src/recorder/cpal.rs @@ -0,0 +1,186 @@ +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{BufferSize, StreamConfig, SampleRate, Host, Device, Stream, SampleFormat}; +use log::{info, warn, error}; + +use once_cell::sync::OnceCell; +use std::sync::Arc; +use arc_swap::ArcSwap; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}; + +use crate::tauri_commands::cpal_data_callback; + +static HOST: OnceCell = OnceCell::new(); +thread_local!(static RECORDER: OnceCell> = OnceCell::new()); +static SELECTED_MICROPHONE_IDX: AtomicI32 = AtomicI32::new(0); +static FRAME_LENGTH: AtomicU32 = AtomicU32::new(0); +static IS_RECORDING: AtomicBool = AtomicBool::new(false); + +pub fn init_microphone(device_index: i32, frame_length: u32) -> bool { + // init host & frame buffer for the callback + if HOST.get().is_none() { + HOST.set(cpal::default_host()); + + // FRAME_BUFFER.set(Mutex::new(vec![0; FRAME_LENGTH.load(Ordering::SeqCst) as usize])); + } + + // init microphone + RECORDER.with(|recorder| { + match recorder.get().is_none() { + true => { + if let Some(device) = get_device(device_index as usize) { + // store + recorder.set(ArcSwap::from_pointee(create_stream(device, frame_length))); + + // remember current configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + // success + true + } else { + false + } + }, + false => { + // check if re-initialization required (i.e. selecetd microphoneor frame-length was changed ) + if SELECTED_MICROPHONE_IDX.load(Ordering::SeqCst) != device_index + || + FRAME_LENGTH.load(Ordering::SeqCst) != frame_length { + warn!("Selected microphone or frame length was changed, re-initializing ..."); + // initialize again with new device index + if IS_RECORDING.load(Ordering::SeqCst) { + stop_recording(); + } + + // remember new configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + if let Some(device) = get_device(device_index as usize) { + // store + recorder.get().unwrap().store(Arc::new(create_stream(device, frame_length))); + + // success + return true + } else { + return false + } + } + + // success + true + } + } + }) +} + +fn create_stream(device: Device, frame_length: u32) -> Stream { + // get default input stream config + // let default_config = device.default_input_config().unwrap(); + + // create config for the stream + // let config: StreamConfig = StreamConfig { + // channels: default_config.channels(), + // sample_rate: SampleRate(16000), + // buffer_size: BufferSize::Fixed(frame_length) + // }; + + let config = device + .default_input_config() + .expect("Failed to load default input config"); + + let channels = config.channels(); + + let err_fn = move |err| { + eprintln!("an error occurred on stream: {}", err); + }; + + match config.sample_format() { + SampleFormat::F32 => device.build_input_stream( + &config.into(), + move |data: &[f32], info| { + cpal_data_callback(data, channels); + }, + err_fn, + None + ), + SampleFormat::U16 => device.build_input_stream( + &config.into(), + move |data: &[u16], info| { + cpal_data_callback(data, channels); + }, + err_fn, + None + ), + SampleFormat::I16 => device.build_input_stream( + &config.into(), + move |data: &[i16], info| { + cpal_data_callback(data, channels); + }, + err_fn, + None + ), + _ => todo!() + }.unwrap() +} + +pub fn stereo_to_mono(input_data: &[i16]) -> Vec { + let mut result = Vec::with_capacity(input_data.len() / 2); + result.extend( + input_data + .chunks_exact(2) + .map(|chunk| chunk[0] / 2 + chunk[1] / 2), + ); + + result +} + +fn get_device(device_index: usize) -> Option { + if let Some(device) = HOST.get().unwrap().input_devices().expect("Get devices error ...").nth(device_index) { + Some(device) + } else { + if let Some(default) = HOST.get().unwrap().default_input_device() { + Some(default) + } else { + error!("No default input device ..."); + + None + } + } +} + +pub fn start_recording(device_index: i32, frame_length: u32) { + // ensure microphone is initialized + init_microphone(device_index, frame_length); + + // start recording + RECORDER.with(|recorder| { + match recorder.get().unwrap().load().play() { + Err(msg) => { + error!("[CPAL] Audio stream PLAY error ... {:?}", msg); + }, + _ => () + }; + + IS_RECORDING.store(true, Ordering::SeqCst); + info!("START recording from microphone ..."); + }); +} + +pub fn stop_recording() { + // ensure microphone is initialized + RECORDER.with(|recorder| { + if !recorder.get().is_none() && IS_RECORDING.load(Ordering::SeqCst) { + // pause instead of stop + match recorder.get().unwrap().load().pause() { + Err(msg) => { + error!("[CPAL] Audio stream PAUSE error ... {:?}", msg); + }, + _ => () + }; + + IS_RECORDING.store(false, Ordering::SeqCst); + info!("STOP recording from microphone ..."); + } + }); +} \ No newline at end of file diff --git a/src-tauri/src/recorder/portaudio.rs b/src-tauri/src/recorder/portaudio.rs new file mode 100644 index 0000000..e69a374 --- /dev/null +++ b/src-tauri/src/recorder/portaudio.rs @@ -0,0 +1,201 @@ +use portaudio as pa; +use pa::{DeviceIndex, Stream}; +use log::{info, warn, error}; + +use once_cell::sync::OnceCell; +use std::sync::{Arc, Mutex}; +use arc_swap::ArcSwap; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}; + +thread_local!(static RECORDER: OnceCell, pa::Input>>>> = OnceCell::new()); +static SELECTED_MICROPHONE_IDX: AtomicI32 = AtomicI32::new(0); +static FRAME_LENGTH: AtomicU32 = AtomicU32::new(0); +static IS_RECORDING: AtomicBool = AtomicBool::new(false); + +const CHANNELS: i32 = 1; +const SAMPLE_RATE: f64 = 16_000.0; + +pub fn init_microphone(device_index: i32, frame_length: u32) -> bool { + RECORDER.with(|r| { + match r.get().is_none() { + true => { + match create_stream(device_index, frame_length) { + Ok(stream) => { + // store + r.set(ArcSwap::from_pointee(Mutex::new(stream))); + + // remember current configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + // success + true + }, + Err(msg) => { + error!("Failed to initialize portaudio.\nError details: {:?}", msg); + + // fail + false + } + } + }, + _ => { + // check if re-initialization required (i.e. selecetd microphoneor frame-length was changed ) + if SELECTED_MICROPHONE_IDX.load(Ordering::SeqCst) != device_index + || + FRAME_LENGTH.load(Ordering::SeqCst) != frame_length { + warn!("Selected microphone or frame length was changed, re-initializing ..."); + // initialize again with new device index + if IS_RECORDING.load(Ordering::SeqCst) { + // RECORDER.get().unwrap().load().stop().expect("Failed to start audio recording!"); + stop_recording(); + } + + // store + match create_stream(device_index, frame_length) { + Ok(stream) => { + // store new stream + r.get().unwrap().store(Arc::new(Mutex::new(stream))); + + // remember new configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + // success + return true + }, + Err(msg) => { + error!("Failed to initialize portaudio.\nError details: {:?}", msg); + + // fail + return false + } + } + } + + // success + true + } + } + }) +} + +fn create_stream(device_index: i32, frame_length: u32) -> Result, pa::Input>, pa::Error> { + let pa_recorder: Result = pa::PortAudio::new(); + + match pa_recorder { + Ok(pa) => { + let input_settings = match get_input_settings(DeviceIndex(device_index as u32), &pa, SAMPLE_RATE, frame_length, CHANNELS) { + Ok(settings) => settings, + Err(error) => panic!("{}", String::from(error)) + }; + + // Construct a stream with input and output sample types of i16 + match pa.open_blocking_stream(input_settings) { + Ok(strm) => Ok(strm), + Err(error) => panic!("{}", error.to_string()), + } + }, + Err(msg) => Err(msg) + } +} + +fn get_input_latency(audio_port: &pa::PortAudio, input_index: pa::DeviceIndex) -> Result +{ + let input_device_information = audio_port.device_info(input_index).or_else(|error| Err(String::from(format!("{}", error)))); + Ok(input_device_information.unwrap().default_low_input_latency) +} + +fn get_input_stream_parameters(input_index: pa::DeviceIndex, latency: f64, channels: i32) -> Result, String> +{ + const INTERLEAVED: bool = true; + Ok(pa::StreamParameters::::new(input_index, channels, INTERLEAVED, latency)) +} + +fn get_input_settings(input_index: pa::DeviceIndex, audio_port: &pa::PortAudio, sample_rate: f64, frames: u32, channels: i32) -> Result, String> +{ + Ok( + pa::InputStreamSettings::new( + (get_input_stream_parameters( + input_index, + (get_input_latency( + &audio_port, + input_index, + ))?, + channels + ))?, + sample_rate, + frames, + ) + ) +} + +// We'll use this function to wait for read/write availability. +fn wait_for_stream(f: F, name: &str) -> u32 +where + F: Fn() -> Result, +{ + loop { + match f() { + Ok(available) => match available { + pa::StreamAvailable::Frames(frames) => return frames as u32, + pa::StreamAvailable::InputOverflowed => println!("Input stream has overflowed"), + pa::StreamAvailable::OutputUnderflowed => { + println!("Output stream has underflowed") + } + }, + Err(err) => panic!( + "An error occurred while waiting for the {} stream: {}", + name, err + ), + } + } +} + +pub fn read_microphone(frame_buffer: &mut [i16]) { + // ensure microphone is initialized + RECORDER.with(|r| { + if !r.get().is_none() { + let cell = r.get().unwrap().load(); + let mut lock = cell.lock(); + let stream = lock.as_mut().unwrap(); + + // read to frame buffer + let in_frames = wait_for_stream(|| stream.read_available(), "Read"); + + if in_frames > 0 { + // let input_samples = stream.read(in_frames).expect("Cannot read frames ..."); + // println!("Read {:?} frames from the input stream.", in_frames); + + let input_samples = stream.read(in_frames).expect("Cannot read frames ..."); + println!("Read: {} (required {})", input_samples.len(), frame_buffer.len()); + frame_buffer.copy_from_slice(input_samples.chunks(frame_buffer.len()).last().unwrap()); + } + // r.get().unwrap().load().read(frame_buffer).expect("Failed to read audio frame"); + } + }); +} + +pub fn start_recording(device_index: i32, frame_length: u32) { + // ensure microphone is initialized + init_microphone(device_index, frame_length); + + // start recording + RECORDER.with(|r| { + r.get().unwrap().load().lock().unwrap().start().expect("Failed to start audio recording!"); + IS_RECORDING.store(true, Ordering::SeqCst); + info!("START recording from microphone ..."); + }); +} + +pub fn stop_recording() { + RECORDER.with(|r| { + if !r.get().is_none() && IS_RECORDING.load(Ordering::SeqCst) { + // stop recording + let pa = r.get().unwrap().load(); + r.get().unwrap().load().lock().unwrap().stop().expect("Failed to stop audio recording!"); + IS_RECORDING.store(false, Ordering::SeqCst); + info!("STOP recording from microphone ..."); + } + }); +} \ No newline at end of file diff --git a/src-tauri/src/recorder/pvrecorder.rs b/src-tauri/src/recorder/pvrecorder.rs new file mode 100644 index 0000000..183a7d2 --- /dev/null +++ b/src-tauri/src/recorder/pvrecorder.rs @@ -0,0 +1,105 @@ +use pv_recorder::{Recorder, RecorderBuilder}; +use log::{info, warn, error}; + +use once_cell::sync::OnceCell; +use std::sync::Arc; +use arc_swap::ArcSwap; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}; + +static RECORDER: OnceCell> = OnceCell::new(); +static SELECTED_MICROPHONE_IDX: AtomicI32 = AtomicI32::new(0); +static FRAME_LENGTH: AtomicU32 = AtomicU32::new(0); +static IS_RECORDING: AtomicBool = AtomicBool::new(false); + +pub fn init_microphone(device_index: i32, frame_length: u32) -> bool { + match RECORDER.get().is_none() { + true => { + let pv_recorder = RecorderBuilder::new() + .device_index(device_index) + .frame_length(frame_length as i32) + .init(); + + match pv_recorder { + Ok(pv) => { + // store + RECORDER.set(ArcSwap::from_pointee(pv)); + + // remember current configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + // success + true + }, + Err(msg) => { + error!("Failed to initialize pvrecorder.\nError details: {:?}", msg); + + // fail + false + } + } + }, + _ => { + // check if re-initialization required (i.e. selecetd microphoneor frame-length was changed ) + if SELECTED_MICROPHONE_IDX.load(Ordering::SeqCst) != device_index + || + RECORDER.get().unwrap().load().frame_length() != frame_length as usize { + warn!("Selected microphone or frame length was changed, re-initializing ..."); + // initialize again with new device index + if IS_RECORDING.load(Ordering::SeqCst) { + // RECORDER.get().unwrap().load().stop().expect("Failed to start audio recording!"); + stop_recording(); + } + + // remember new configuration + SELECTED_MICROPHONE_IDX.store(device_index, Ordering::SeqCst); + FRAME_LENGTH.store(frame_length, Ordering::SeqCst); + + // store + RECORDER.get().unwrap().store(Arc::new(RecorderBuilder::new() + .device_index(device_index) + .frame_length(frame_length as i32) + .init() + .expect("Failed to initialize pvrecorder"))); + } + + // success + true + } + } +} + +pub fn read_microphone(frame_buffer: &mut [i16]) { + // ensure microphone is initialized + if !RECORDER.get().is_none() { + // read to frame buffer + match RECORDER.get().unwrap().load().read(frame_buffer) { + Err(msg) => { + // @TODO: Fix somehow. PvRecorder always wait for PCM buffer size of 512. + // error!("Failed to read audio frame. {:?}", msg); + // eprintln!("Failed to read audio frame. {:?}", msg); + }, + _ => () + } + } +} + +pub fn start_recording(device_index: i32, frame_length: u32) { + // ensure microphone is initialized + init_microphone(device_index, frame_length); + + // start recording + RECORDER.get().unwrap().load().start().expect("Failed to start audio recording!"); + IS_RECORDING.store(true, Ordering::SeqCst); + info!("START recording from microphone ..."); +} + +pub fn stop_recording() { + // ensure microphone is initialized + if !RECORDER.get().is_none() && IS_RECORDING.load(Ordering::SeqCst) { + // stop recording + RECORDER.get().unwrap().load().stop().expect("Failed to stop audio recording!"); + IS_RECORDING.store(false, Ordering::SeqCst); + info!("STOP recording from microphone ..."); + } +} \ No newline at end of file diff --git a/src-tauri/src/tauri_commands.rs b/src-tauri/src/tauri_commands.rs index 398b0bb..250928d 100644 --- a/src-tauri/src/tauri_commands.rs +++ b/src-tauri/src/tauri_commands.rs @@ -3,8 +3,8 @@ mod db; pub use db::*; // import RECORDER commands -mod recorder; -pub use recorder::*; +mod audio; +pub use audio::*; // import PORCUPINE commands mod listener; diff --git a/src-tauri/src/tauri_commands/recorder.rs b/src-tauri/src/tauri_commands/audio.rs similarity index 100% rename from src-tauri/src/tauri_commands/recorder.rs rename to src-tauri/src/tauri_commands/audio.rs diff --git a/src-tauri/src/tauri_commands/listener.rs b/src-tauri/src/tauri_commands/listener.rs index c535d56..5367780 100644 --- a/src-tauri/src/tauri_commands/listener.rs +++ b/src-tauri/src/tauri_commands/listener.rs @@ -2,19 +2,23 @@ use porcupine::{Porcupine, PorcupineBuilder}; use std::sync::atomic::{AtomicBool, Ordering}; use std::path::Path; use log::{info, warn, error}; +use rustpotter::{Rustpotter, RustpotterConfig, WavFmt, DetectorConfig, FiltersConfig, ScoreMode, GainNormalizationConfig, BandPassConfig}; +// use dasp::{sample::ToSample, Sample}; // use crate::events::Payload; use tauri::Manager; use rand::seq::SliceRandom; use std::time::SystemTime; +use once_cell::sync::OnceCell; +use std::sync::Mutex; use crate::assistant_commands; use crate::events; use crate::config; use crate::vosk; -use crate::recorder; +use crate::recorder::{self, FRAME_LENGTH}; use crate::COMMANDS; use crate::DB; @@ -25,6 +29,15 @@ static LISTENING: AtomicBool = AtomicBool::new(false); // stop listening with Atomic flag (to make it work between different threads) static STOP_LISTENING: AtomicBool = AtomicBool::new(false); +// store tauri app_handle +static TAURI_APP_HANDLE: OnceCell = OnceCell::new(); + +// store porcupine instance +static PORCUPINE: OnceCell = OnceCell::new(); + +// store rustpotter instance +static RUSTPOTTER: OnceCell> = OnceCell::new(); + #[tauri::command] pub fn is_listening() -> bool { LISTENING.load(Ordering::SeqCst) @@ -34,12 +47,31 @@ pub fn is_listening() -> bool { pub fn stop_listening() { if is_listening() { STOP_LISTENING.store(true, Ordering::SeqCst); + stop_recording(); } // wait until listening stops while is_listening() {} } +fn get_wake_word_engine() -> config::WakeWordEngine { + let selected_wake_word_engine; + if let Some(wwengine) = DB.lock().unwrap().get::("selected_wake_word_engine") { + // from db + match wwengine.trim().to_lowercase().as_str() { + "rustpotter" => selected_wake_word_engine = config::WakeWordEngine::Rustpotter, + "vosk" => selected_wake_word_engine = config::WakeWordEngine::Vosk, + "picovoice" => selected_wake_word_engine = config::WakeWordEngine::Porcupine, + &_ => todo!() + } + } else { + // default + selected_wake_word_engine = config::DEFAULT_WAKE_WORD_ENGINE; // set default wake_word engine + } + + selected_wake_word_engine +} + #[tauri::command(async)] pub fn start_listening(app_handle: tauri::AppHandle) -> Result { // only one listener thread is allowed @@ -47,44 +79,29 @@ pub fn start_listening(app_handle: tauri::AppHandle) -> Result { return Err("Already listening.".into()); } - // Retrieve selected wake-word engine from DB - let selected_wake_word_engine; - if let Some(wwengine) = DB.lock().unwrap().get::("selected_wake_word_engine") { - // from db - selected_wake_word_engine = wwengine; - } else { - // default - selected_wake_word_engine = config::WAKE_WORD_ENGINES.first().expect("No wake-word engines found ...").to_string(); // set default wake_word engine + // keep app handle + if TAURI_APP_HANDLE.get().is_none() { + TAURI_APP_HANDLE.set(app_handle); } // call selected wake-word engine listener command - match selected_wake_word_engine.as_str() { - "rustpotter" => { - info!("Starting rustpotter wake-word engine ..."); - return picovoice_listen(&app_handle, |_app| { - // Greet user - events::play("run", &app_handle); - }, |app, kidx| keyword_callback(app, kidx)); + match get_wake_word_engine() { + config::WakeWordEngine::Rustpotter => { + info!("Starting RUSTPOTTER wake-word engine ..."); + return rustpotter_init(); }, - "vosk" => { - info!("Starting vosk wake-word engine ..."); - return vosk_listen(&app_handle, |_app| { - // Greet user - events::play("run", &app_handle); - }, |app, kidx| keyword_callback(app, kidx)); + config::WakeWordEngine::Vosk => { + info!("Starting VOSK wake-word engine ..."); + return vosk_init(); }, - "picovoice" => { - info!("Starting picovoice wake-word engine ..."); - return picovoice_listen(&app_handle, |_app| { - // Greet user - events::play("run", &app_handle); - }, |app, kidx| keyword_callback(app, kidx)); - }, - _ => Err("No wake-word engine selected ...".into()) + config::WakeWordEngine::Porcupine => { + info!("Starting PICOVOICE PORCUPINE wake-word engine ..."); + return picovoice_init(); + } } } -pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { +fn keyword_callback(_keyword_index: i32) { // vars let mut start: SystemTime = SystemTime::now(); let mut frame_buffer = vec![0; recorder::FRAME_LENGTH.load(Ordering::SeqCst) as usize]; @@ -94,11 +111,11 @@ pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { config::ASSISTANT_GREET_PHRASES .choose(&mut rand::thread_rng()) .unwrap(), - &app_handle, + TAURI_APP_HANDLE.get().unwrap(), ); // emit assistant greet event - app_handle + TAURI_APP_HANDLE.get().unwrap() .emit_all(events::EventTypes::AssistantGreet.get(), ()) .unwrap(); @@ -128,7 +145,7 @@ pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { let cmd_result = assistant_commands::execute_command( &cmd_path, &cmd_config, - &app_handle, + TAURI_APP_HANDLE.get().unwrap(), ); match cmd_result { @@ -142,7 +159,7 @@ pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { } } - app_handle + TAURI_APP_HANDLE.get().unwrap() .emit_all(events::EventTypes::AssistantWaiting.get(), ()) .unwrap(); break; // return to picovoice after command execution (no matter successfull or not) @@ -153,7 +170,7 @@ pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { match start.elapsed() { Ok(elapsed) if elapsed > config::CMS_WAIT_DELAY => { // return to picovoice after N seconds - app_handle + TAURI_APP_HANDLE.get().unwrap() .emit_all(events::EventTypes::AssistantWaiting.get(), ()) .unwrap(); break; @@ -163,60 +180,191 @@ pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) { } } -pub fn vosk_listen<'s, S, K>(app_handle: &tauri::AppHandle, start_callback: S, mut keyword_callback: K) -> Result - where S: Fn(&tauri::AppHandle), - K: FnMut(&tauri::AppHandle, i32) { +pub fn data_callback(frame_buffer: &[i16]) { + // println!("DATA CALLBACK {}", frame_buffer.len()); + match get_wake_word_engine() { + config::WakeWordEngine::Rustpotter => { + let mut lock = RUSTPOTTER.get().unwrap().lock(); + let rustpotter = lock.as_mut().unwrap(); + let detection = rustpotter.process_i16(&frame_buffer); - // vars - let fetch_phrase = "джарвис".chars().collect::>(); - let frame_length: usize = 128; - let min_ratio: f64 = 0.8; + if let Some(detection) = detection { + if detection.score > config::RUSPOTTER_MIN_SCORE { + info!("Rustpotter detection info:\n{:?}", detection); + keyword_callback(0); + } else { + info!("Rustpotter detection info:\n{:?}", detection); + } + } + }, + config::WakeWordEngine::Vosk => { + // recognize & convert to sequence + let recognized_phrase = vosk::recognize(&frame_buffer, true).unwrap_or("".into()); - // Start recording - let mut frame_buffer = vec![0; frame_length]; - recorder::FRAME_LENGTH.store(frame_length as u32, Ordering::SeqCst); - recorder::start_recording(); - LISTENING.store(true, Ordering::SeqCst); + if !recognized_phrase.trim().is_empty() { + info!("Rec: {}", recognized_phrase); + let recognized_phrases = recognized_phrase.split_whitespace(); + for phrase in recognized_phrases { + let recognized_phrase_chars = phrase.trim().to_lowercase().chars().collect::>(); + + // compare + let compare_ratio = seqdiff::ratio(&config::VOSK_FETCH_PHRASE.chars().collect::>(), &recognized_phrase_chars); + info!("OG phrase: {:?}", &config::VOSK_FETCH_PHRASE); + info!("Recognized phrase: {:?}", &recognized_phrase_chars); + info!("Compare ratio: {}", compare_ratio); - // run start callback - start_callback(app_handle); - - // Listen until stop flag will be true - while !STOP_LISTENING.load(Ordering::SeqCst) { - recorder::read_microphone(&mut frame_buffer); - - // recognize & convert to sequence - let recognized_phrase = vosk::recognize(&frame_buffer, true).unwrap_or("".into()); - - if !recognized_phrase.trim().is_empty() { - info!("Rec: {}", recognized_phrase); - let recognized_phrases = recognized_phrase.split_whitespace(); - for phrase in recognized_phrases { - let recognized_phrase_chars = phrase.trim().to_lowercase().chars().collect::>(); - - // compare - if seqdiff::ratio(&fetch_phrase, &recognized_phrase_chars) >= min_ratio { - info!("Phrase: {:?}", &fetch_phrase); - info!("Compare: {:?}", &recognized_phrase_chars); - keyword_callback(&app_handle, 0); - break; + if compare_ratio >= config::VOSK_MIN_RATIO { + info!("Phrase activated."); + keyword_callback(0); + break; + } + } + } + }, + config::WakeWordEngine::Porcupine => { + if let Ok(keyword_index) = PORCUPINE.get().unwrap().process(&frame_buffer) { + if keyword_index >= 0 { + // println!("Yes, sir! {}", keyword_index); + keyword_callback(keyword_index); } } } } - - // Stop listening - recorder::stop_recording(); - LISTENING.store(false, Ordering::SeqCst); - STOP_LISTENING.store(false, Ordering::SeqCst); - - Ok(true) } -pub fn picovoice_listen<'s, S, K>(app_handle: &tauri::AppHandle, start_callback: S, mut keyword_callback: K) -> Result - where S: Fn(&tauri::AppHandle), - K: FnMut(&tauri::AppHandle, i32) { +fn start_recording() -> Result { + // vars + let frame_length: usize; + // idenfity frame length + match get_wake_word_engine() { + config::WakeWordEngine::Rustpotter => { + // start recording for Rustpotter + // You need a buffer of size `rustpotter.get_samples_per_frame()` when using samples. + // You need a buffer of size `rustpotter.get_bytes_per_frame()` when using bytes. + frame_length = RUSTPOTTER.get().unwrap().lock().unwrap().get_samples_per_frame(); + recorder::FRAME_LENGTH.store(frame_length as u32, Ordering::SeqCst); + }, + config::WakeWordEngine::Vosk => { + // start recording for Vosk + frame_length = 128; + recorder::FRAME_LENGTH.store(frame_length as u32, Ordering::SeqCst); + }, + config::WakeWordEngine::Porcupine => { + // start recording for Porcupine + frame_length = PORCUPINE.get().unwrap().frame_length() as usize; + recorder::FRAME_LENGTH.store(PORCUPINE.get().unwrap().frame_length(), Ordering::SeqCst); + } + } + + // define frame buffer + let mut frame_buffer: Vec = vec![0; frame_length]; + + // init stuff + recorder::init(); // init + recorder::start_recording(); // start + LISTENING.store(true, Ordering::SeqCst); + info!("START listening ..."); + + // greet user + events::play("run", TAURI_APP_HANDLE.get().unwrap()); + + // record + match recorder::RECORDER_TYPE.load(Ordering::SeqCst) { + recorder::RecorderType::PvRecorder => { + while !STOP_LISTENING.load(Ordering::SeqCst) { + recorder::read_microphone(&mut frame_buffer); + data_callback(&frame_buffer); + } + + // stop + stop_recording(); + + Ok(true) + }, + recorder::RecorderType::PortAudio => { + while !STOP_LISTENING.load(Ordering::SeqCst) { + recorder::read_microphone(&mut frame_buffer); + data_callback(&frame_buffer); + } + + // stop + stop_recording(); + + Ok(true) + } + recorder::RecorderType::Cpal => { + todo!() + } + } +} + +fn stop_recording() { + // Stop listening + recorder::stop_recording(); + + LISTENING.store(false, Ordering::SeqCst); + STOP_LISTENING.store(false, Ordering::SeqCst); + info!("STOP listening ..."); +} + +fn rustpotter_init() -> Result { + + // init rustpotter + let rustpotter_config = RustpotterConfig { + fmt: WavFmt::default(), + detector: DetectorConfig { + avg_threshold: 0., + threshold: 0.5, + min_scores: 15, + score_mode: ScoreMode::Max, + comparator_band_size: 5, + comparator_ref: 0.22 + }, + filters: FiltersConfig { + gain_normalizer: GainNormalizationConfig { + enabled: true, + gain_ref: None, + min_gain: 0.5, + max_gain: 1.0, + }, + band_pass: BandPassConfig { + enabled: true, + low_cutoff: 80., + high_cutoff: 400., + } + } + }; + let mut rustpotter = Rustpotter::new(&rustpotter_config).unwrap(); + + // load a wakeword + let rustpotter_wake_word_files: [&str; 5] = [ + "rustpotter/jarvis-default.rpw", + "rustpotter/jarvis-community-1.rpw", + "rustpotter/jarvis-community-2.rpw", + "rustpotter/jarvis-community-3.rpw", + "rustpotter/jarvis-community-4.rpw", + // "rustpotter/jarvis-community-5.rpw", + ]; + + for rpw in rustpotter_wake_word_files { + rustpotter.add_wakeword_from_file(rpw).unwrap(); + } + + // store rustpotter + if RUSTPOTTER.get().is_none() { + RUSTPOTTER.set(Mutex::new(rustpotter)); + } + + // start recording + start_recording() +} + +fn vosk_init() -> Result { + start_recording() +} + +fn picovoice_init() -> Result { // VARS let porcupine: Porcupine; let picovoice_api_key: String; @@ -248,31 +396,11 @@ pub fn picovoice_listen<'s, S, K>(app_handle: &tauri::AppHandle, start_callback: } } - // Start recording - let mut frame_buffer = vec![0; porcupine.frame_length() as usize]; - recorder::FRAME_LENGTH.store(porcupine.frame_length(), Ordering::SeqCst); - recorder::start_recording(); - LISTENING.store(true, Ordering::SeqCst); - - // run start callback - start_callback(app_handle); - - // Listen until stop flag will be true - while !STOP_LISTENING.load(Ordering::SeqCst) { - recorder::read_microphone(&mut frame_buffer); - - if let Ok(keyword_index) = porcupine.process(&frame_buffer) { - if keyword_index >= 0 { - // println!("Yes, sir! {}", keyword_index); - keyword_callback(&app_handle, keyword_index); - } - } + // store + if PORCUPINE.get().is_none() { + PORCUPINE.set(porcupine); } - // Stop listening - recorder::stop_recording(); - LISTENING.store(false, Ordering::SeqCst); - STOP_LISTENING.store(false, Ordering::SeqCst); - - Ok(true) -} + // start recording + start_recording() +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6b6559f..3ec137c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "jarvis-app", - "version": "0.0.1" + "version": "0.0.2" }, "tauri": { "allowlist": { @@ -41,6 +41,7 @@ "sound", "vosk/model_small", "picovoice", + "rustpotter", "libvosk.dll", "libstdc++-6.dll", "libwinpthread-1.dll", diff --git a/src/pages/settings.svelte b/src/pages/settings.svelte index 2552871..0322166 100644 --- a/src/pages/settings.svelte +++ b/src/pages/settings.svelte @@ -16,6 +16,7 @@ // VARIABLES let available_microphones = []; let settings_saved = false; + let save_button_disabled = false; let assistant_voice_val = ""; // shared let selected_microphone = ""; @@ -32,6 +33,7 @@ // FUNCTIONS async function save_settings() { + save_button_disabled = true; // disable save button for a while settings_saved = false; // hide alert await invoke("db_write", {key: "assistant_voice", val: assistant_voice_val}); @@ -49,6 +51,10 @@ settings_saved = false; // hide alert again after N seconds }, 5000); + setTimeout(() => { + save_button_disabled = false; // enable save button again + }, 1000); + // restart listening everytime new settings is saved stopListening(() => { startListening(); @@ -157,7 +163,7 @@ -