Skip to content

Commit

Permalink
Add sound effect
Browse files Browse the repository at this point in the history
  • Loading branch information
AurevoirXavier committed Jul 9, 2024
1 parent d3feddd commit 323734e
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 18 deletions.
306 changes: 304 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ futures = { version = "0.3" }
global-hotkey = { version = "0.5" }
parking_lot = { version = "0.12" }
reqwew = { version = "0.2" }
rodio = { version = "0.19" }
serde = { version = "1.0", features = ["derive"] }
thiserror = { version = "1.0" }
tokio = { version = "1.38", features = ["rt-multi-thread"] }
Expand Down
Binary file added asset/notification.mp3
Binary file not shown.
2 changes: 2 additions & 0 deletions src/component.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod audio;

pub mod function;

pub mod keyboard;
Expand Down
43 changes: 43 additions & 0 deletions src/component/audio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// std
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
io::Cursor,
};
// crates.io
use rodio::{
source::{Buffered, Source as _},
Decoder, OutputStream, OutputStreamHandle, Sink,
};
// self
use crate::prelude::*;

type Source = Buffered<Decoder<Cursor<&'static [u8]>>>;

pub struct Audio {
pub notification: Source,
sink: Sink,
// Stream must be kept alive.
_stream: (OutputStream, OutputStreamHandle),
}
impl Audio {
pub fn new() -> Result<Self> {
let sound_data = include_bytes!("../../asset/notification.mp3");
let cursor = Cursor::new(sound_data.as_ref());
let decoder = Decoder::new(cursor).map_err(RodioError::Decoder)?;
let notification = decoder.buffered();
let _stream = OutputStream::try_default().map_err(RodioError::Stream)?;
let sink = Sink::try_new(&_stream.1).map_err(RodioError::Play)?;

Ok(Audio { notification, sink, _stream })
}

pub fn play(&self, audio_src: Source) {
self.sink.append(audio_src);
self.sink.sleep_until_end();
}
}
impl Debug for Audio {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "Audio(..)")
}
}
2 changes: 1 addition & 1 deletion src/component/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl OpenAi {
let msg = [
ChatCompletionRequestSystemMessageArgs::default().content(prompt).build()?.into(),
ChatCompletionRequestUserMessageArgs::default()
.content(format!("```AiR\n{content}\n```"))
.content(format!("<AiR>\n{content}\n</AiR>"))
.build()?
.into(),
];
Expand Down
23 changes: 13 additions & 10 deletions src/component/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ pub struct Rewrite {
}
impl Rewrite {
pub fn prompt(&self) -> Cow<str> {
const DEFAULT: &str = "As a language professor, assist me in refining text! \
const DEFAULT: &str =
"As a professional writer and language master, assist me in refining text! \
Amend any grammatical errors and enhance the language to sound more like a native speaker! \
Text is always provided in format ```AiR\n$TEXT\n```! \
Text is always provided in format `<AiR>$TEXT</AiR>`! \
$TEXT can be provided in any style! \
Discard the ```AiR\n\n``` surroundings! \
Discard the `<AiR></AiR>` tag! \
Extract the $TEXT and return the refined $TEXT only!";

if self.additional_prompt.is_empty() {
Expand All @@ -119,11 +120,13 @@ pub struct Translation {
impl Translation {
pub fn prompt(&self) -> Cow<str> {
let default = format!(
"As a language professor, assist me in translate text between {} and {}! \
"As a professional translator and language master, assist me in translating text! \
I provide two languages, {} and {}! \
Determine which language the text I give is in, and then translate accordingly. \
Amend any grammatical errors and enhance the language to sound more like a native speaker! \
Text is always provided in format ```AiR\n$TEXT\n```! \
Text is always provided in format `<AiR>$TEXT</AiR>`! \
$TEXT can be provided in any style! \
Discard the ```AiR\n\n``` surroundings! \
Discard the `<AiR></AiR>` tag! \
Extract the $TEXT and return the translated $TEXT only!",
self.a.as_str(),
self.b.as_str(),
Expand Down Expand Up @@ -180,10 +183,10 @@ pub struct Hotkeys {
impl Default for Hotkeys {
fn default() -> Self {
Self {
rewrite: "ctrl+y".into(),
rewrite_directly: "ctrl+u".into(),
translate: "ctrl+i".into(),
translate_directly: "ctrl+o".into(),
rewrite: "ctrl+t".into(),
rewrite_directly: "ctrl+y".into(),
translate: "ctrl+u".into(),
translate_directly: "ctrl+i".into(),
}
}
}
12 changes: 12 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub enum Error {
Enigo(#[from] EnigoError),
#[error(transparent)]
GlobalHotKey(#[from] GlobalHotKeyError),
#[error(transparent)]
Rodio(#[from] RodioError),
#[error("unsupported key: {0}")]
UnsupportedKey(String),
}
Expand All @@ -41,3 +43,13 @@ pub enum GlobalHotKeyError {
#[error(transparent)]
Parse(#[from] global_hotkey::hotkey::HotKeyParseError),
}

#[derive(Debug, thiserror::Error)]
pub enum RodioError {
#[error(transparent)]
Decoder(#[from] rodio::decoder::DecoderError),
#[error(transparent)]
Play(#[from] rodio::PlayError),
#[error(transparent)]
Stream(#[from] rodio::StreamError),
}
9 changes: 8 additions & 1 deletion src/service.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod audio;
use audio::Audio;

mod chat;
use chat::Chat;

Expand Down Expand Up @@ -28,6 +31,7 @@ pub struct Services {
pub quoter: Quoter,
pub is_chatting: Arc<AtomicBool>,
pub chat: Chat,
pub audio: Audio,
pub hotkey: Hotkey,
}
impl Services {
Expand All @@ -38,15 +42,17 @@ impl Services {
let is_chatting = Arc::new(AtomicBool::new(false));
let chat =
Chat::new(keyboard.clone(), &rt, is_chatting.clone(), &components.setting, &state.chat);
let audio = Audio::new()?;
let hotkey = Hotkey::new(
ctx,
keyboard.clone(),
&components.setting.hotkeys,
state.general.hide_on_lost_focus.clone(),
audio.clone(),
chat.tx.clone(),
)?;

Ok(Self { keyboard, rt: Some(rt), quoter, is_chatting, chat, hotkey })
Ok(Self { keyboard, rt: Some(rt), quoter, is_chatting, chat, audio, hotkey })
}

pub fn is_chatting(&self) -> bool {
Expand All @@ -57,6 +63,7 @@ impl Services {
self.keyboard.abort();
self.quoter.abort();
self.chat.abort();
self.audio.abort();
self.hotkey.abort();

if let Some(rt) = self.rt.take() {
Expand Down
48 changes: 48 additions & 0 deletions src/service/audio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// std
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
sync::mpsc::{self, Sender},
thread,
};
// self
use crate::{component::audio::Audio as A, prelude::*};

#[derive(Clone)]
pub struct Audio(Sender<Effect>);
impl Audio {
pub fn new() -> Result<Self> {
let (tx, rx) = mpsc::channel::<Effect>();

thread::spawn(move || {
let audio = A::new().expect("audio must be created");

loop {
match rx.recv().expect("receive must succeed") {
Effect::Notification => audio.play(audio.notification.clone()),
Effect::Abort => return,
}
}
});

Ok(Self(tx))
}

pub fn play_notification(&self) {
self.0.send(Effect::Notification).expect("send must succeed");
}

pub fn abort(&self) {
self.0.send(Effect::Abort).expect("send must succeed");
}
}
impl Debug for Audio {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "Audio(..)")
}
}

#[derive(Debug)]
enum Effect {
Notification,
Abort,
}
11 changes: 7 additions & 4 deletions src/service/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use arboard::Clipboard;
use eframe::egui::{Context, ViewportCommand};
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState};
// self
use super::{chat::ChatArgs, keyboard::Keyboard};
use super::{audio::Audio, chat::ChatArgs, keyboard::Keyboard};
use crate::{
component::{function::Function, keyboard::Keys, setting::Hotkeys},
os::*,
Expand All @@ -28,6 +28,7 @@ impl Hotkey {
keyboard: Keyboard,
hotkeys: &Hotkeys,
hide_on_lost_focus: Arc<AtomicBool>,
audio: Audio,
tx: Sender<ChatArgs>,
) -> Result<Self> {
let ctx = ctx.to_owned();
Expand All @@ -39,7 +40,7 @@ impl Hotkey {

// TODO: handle the error.
thread::spawn(move || {
// The manager need to be kept alive during the whole program life.
// Manager must be kept alive.
let manager = manager;

while !abort_.load(Ordering::Relaxed) {
Expand All @@ -48,6 +49,8 @@ impl Hotkey {

// We don't care about the release event.
if let HotKeyState::Pressed = e.state {
audio.play_notification();

let (func, keys) = manager.match_func(e.id);
let to_focus = !func.is_directly();

Expand All @@ -60,12 +63,12 @@ impl Hotkey {
// operation successfully.
keyboard.release_keys(keys);
// Give system some time to response `releases_keys`.
thread::sleep(Duration::from_millis(200));
thread::sleep(Duration::from_millis(250));

keyboard.copy();

// Give some time to the system to refresh the clipboard.
thread::sleep(Duration::from_millis(500));
thread::sleep(Duration::from_millis(250));

let content = match clipboard.get_text() {
Ok(c) if !c.is_empty() => c,
Expand Down

0 comments on commit 323734e

Please sign in to comment.