Recorder rewritten. + Attempt to integrate cpal and portaudio.

This commit is contained in:
Abraham 2023-04-30 22:32:48 +05:00
parent 20081161f3
commit 85eb06fa06
23 changed files with 983 additions and 183 deletions

View file

@ -0,0 +1,2 @@
[env]
PORTAUDIO_ONLY_STATIC = true

View file

@ -3,6 +3,7 @@ use seqdiff::ratio;
use serde_yaml;
use std::path::Path;
use std::{fs, fs::File};
use log::{info, warn, error};
use core::time::Duration;
use std::path::PathBuf;
@ -36,7 +37,7 @@ pub fn parse_commands() -> Result<Vec<AssistantCommand>, String> {
if let Ok(parse_result) = serde_yaml::from_reader::<File, CommandsList>(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<Vec<AssistantCommand>, 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())
},
}
}

View file

@ -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);

View file

@ -33,6 +33,11 @@ lazy_static! {
static ref APP_CONFIG_DIR: Mutex<String> = Mutex::new(String::new());
}
// data dir
lazy_static! {
static ref APP_LOG_DIR: Mutex<String> = Mutex::new(String::new());
}
// init PickleDb connection
lazy_static! {
static ref DB: Mutex<PickleDb> = 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![

View file

@ -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<Recorder> = 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
}

View file

@ -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<Host> = OnceCell::new();
thread_local!(static RECORDER: OnceCell<ArcSwap<Stream>> = 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<i16> {
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<Device> {
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 ...");
}
});
}

View file

@ -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<ArcSwap<Mutex<Stream<pa::Blocking<pa::stream::Buffer>, pa::Input<i16>>>>> = 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<Stream<pa::Blocking<pa::stream::Buffer>, pa::Input<i16>>, pa::Error> {
let pa_recorder: Result<pa::PortAudio, pa::Error> = 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<f64, String>
{
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<pa::StreamParameters<i16>, String>
{
const INTERLEAVED: bool = true;
Ok(pa::StreamParameters::<i16>::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<pa::InputStreamSettings<i16>, 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: F, name: &str) -> u32
where
F: Fn() -> Result<pa::StreamAvailable, pa::error::Error>,
{
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 ...");
}
});
}

View file

@ -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<ArcSwap<Recorder>> = 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 ...");
}
}

View file

@ -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;

View file

@ -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<tauri::AppHandle> = OnceCell::new();
// store porcupine instance
static PORCUPINE: OnceCell<Porcupine> = OnceCell::new();
// store rustpotter instance
static RUSTPOTTER: OnceCell<Mutex<Rustpotter>> = 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::<String>("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<bool, String> {
// only one listener thread is allowed
@ -47,44 +79,29 @@ pub fn start_listening(app_handle: tauri::AppHandle) -> Result<bool, String> {
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::<String>("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<bool, String>
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::<Vec<_>>();
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::<Vec<_>>();
// compare
let compare_ratio = seqdiff::ratio(&config::VOSK_FETCH_PHRASE.chars().collect::<Vec<_>>(), &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::<Vec<_>>();
// 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<bool, String>
where S: Fn(&tauri::AppHandle),
K: FnMut(&tauri::AppHandle, i32) {
fn start_recording() -> Result<bool, String> {
// 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<i16> = 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<bool, String> {
// 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<bool, String> {
start_recording()
}
fn picovoice_init() -> Result<bool, String> {
// 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()
}