Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 476 additions & 14 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ gif = "0.13.3"
gif-dispose = "5.0.1"
sha2 = "0.10"
webp-animation = "0.9.0"
rodio = { version = "0.19", default-features = false }
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis", "wav", "aiff", "mp3", "isomp4", "aac", "pcm", "adpcm", "flac", "alac"] }

[target."cfg(windows)".dependencies]
windows = { version = "0.61.3", features = [
Expand All @@ -48,6 +50,7 @@ windows = { version = "0.61.3", features = [
"Win32_Security",
"Win32_UI_Shell"
] }
thread-priority = "1.2"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
semver = "1"
self-replace = "1.5"
Expand All @@ -57,6 +60,7 @@ tempfile = "3"
rdev = "0.5.3"
objc = "0.2"
objc-foundation = "0.1"
thread-priority = "1.2"

# objc 크레이트의 매크로에서 발생하는 cfg 경고 억제
[lints.rust]
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/gen/schemas/acl-manifests.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src-tauri/permissions/dmnote-allow-all.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
"css_tab_toggle",
"font_load",
"image_load",
"sound_load",
"sound_list",
"sound_set_enabled",
"sound_delete",
"sound_save_processed_wav",
"sound_load_original",
"sound_update_processed_wav",
"js_get",
"js_get_use",
"js_toggle",
Expand All @@ -57,6 +64,12 @@
"keys_set_counters",
"raw_input_subscribe",
"raw_input_unsubscribe",
"key_sound_get_status",
"key_sound_set_enabled",
"key_sound_set_volume",
"key_sound_load_soundpack",
"key_sound_unload_soundpack",
"key_sound_set_latency_logging",
"positions_get",
"positions_update",
"custom_tabs_list",
Expand Down
146 changes: 146 additions & 0 deletions src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use std::{
thread::{self, JoinHandle},
time::Duration,
};
#[cfg(debug_assertions)]
use std::time::Instant;

use anyhow::{anyhow, Context, Result};
use log::{error, warn};
Expand All @@ -23,6 +25,7 @@ use tauri::{
use tauri_runtime_wry::wry::dpi::{LogicalPosition, LogicalSize};

use crate::{
key_sound::{KeySoundEngine, KeySoundStatus},
keyboard::KeyboardManager,
models::{
overlay_resize_anchor_from_str, BootstrapOverlayState, BootstrapPayload, KeyCounters,
Expand All @@ -31,6 +34,8 @@ use crate::{
services::{css_watcher::CssWatcher, settings::SettingsService},
store::AppStore,
};
#[cfg(debug_assertions)]
use crate::key_sound::KeySoundDispatchTrace;

const OVERLAY_LABEL: &str = "overlay";
const TRAY_ICON_ID: &str = "background-tray";
Expand All @@ -52,6 +57,7 @@ pub struct AppState {
active_keys: Arc<RwLock<HashSet<String>>>,
/// Raw input stream subscriber count - emit only when > 0
raw_input_subscribers: Arc<std::sync::atomic::AtomicU32>,
key_sound: Arc<KeySoundEngine>,
/// CSS 파일 핫리로딩 워처
css_watcher: RwLock<Option<CssWatcher>>,
}
Expand All @@ -68,6 +74,7 @@ impl AppState {
Self::sync_counters_with_keys_impl(&key_counters, &snapshot.keys);
let key_counter_enabled = Arc::new(AtomicBool::new(snapshot.key_counter_enabled));
let active_keys = Arc::new(RwLock::new(HashSet::new()));
let key_sound = Arc::new(KeySoundEngine::new());

Ok(Self {
store,
Expand All @@ -80,6 +87,7 @@ impl AppState {
key_counter_enabled,
active_keys,
raw_input_subscribers: Arc::new(std::sync::atomic::AtomicU32::new(0)),
key_sound,
css_watcher: RwLock::new(None),
})
}
Expand Down Expand Up @@ -695,6 +703,76 @@ impl AppState {
if !state_changed {
continue;
}
if message.device == crate::ipc::InputDeviceKind::Keyboard
&& state == "DOWN"
{
if let Some((sound_path, per_key_volume)) =
app_state.resolve_key_sound_binding(&mode, &key_label)
{
#[cfg(debug_assertions)]
let key_sound_input_started_at = Instant::now();
#[cfg(debug_assertions)]
let key_sound_dispatch_started_at = Instant::now();
#[cfg(debug_assertions)]
let dispatch_ms =
key_sound_dispatch_started_at.elapsed().as_secs_f64()
* 1000.0;
#[cfg(debug_assertions)]
let trace = KeySoundDispatchTrace::new(
key_sound_input_started_at,
dispatch_ms,
);
app_state.key_sound.play_file(
&sound_path,
per_key_volume,
#[cfg(debug_assertions)]
Some(trace),
#[cfg(not(debug_assertions))]
None,
);
#[cfg(debug_assertions)]
if app_state.key_sound.latency_logging_enabled() {
log::debug!(
"[KeySound][Latency] route=dispatch dispatchMs={dispatch_ms:.3} mode={} key={} volume={:.3} path={}",
mode,
key_label,
per_key_volume,
sound_path
);
}
} else {
#[cfg(debug_assertions)]
let key_sound_input_started_at = Instant::now();
#[cfg(debug_assertions)]
let key_sound_dispatch_started_at = Instant::now();
#[cfg(debug_assertions)]
let dispatch_ms =
key_sound_dispatch_started_at.elapsed().as_secs_f64()
* 1000.0;
#[cfg(debug_assertions)]
let trace = KeySoundDispatchTrace::new(
key_sound_input_started_at,
dispatch_ms,
);
app_state
.key_sound
.play_labels(
&message.labels,
#[cfg(debug_assertions)]
Some(trace),
#[cfg(not(debug_assertions))]
None,
);
#[cfg(debug_assertions)]
if app_state.key_sound.latency_logging_enabled() {
log::debug!(
"[KeySound][Latency] route=dispatch dispatchMs={dispatch_ms:.3} mode={} key={} source=soundpack",
mode,
key_label
);
}
}
}
let payload = json!({ "key": key_label, "state": state, "mode": mode });

let mut emitted = false;
Expand Down Expand Up @@ -1185,6 +1263,74 @@ impl AppState {
self.raw_input_subscribers.load(Ordering::Relaxed)
}

pub fn key_sound_status(&self) -> KeySoundStatus {
self.key_sound.status()
}

pub fn key_sound_set_enabled(&self, enabled: bool) -> KeySoundStatus {
self.key_sound.set_enabled(enabled)
}

pub fn key_sound_set_volume(&self, volume: f32) -> KeySoundStatus {
self.key_sound.set_volume(volume)
}

pub fn key_sound_set_latency_logging(&self, enabled: bool) -> KeySoundStatus {
self.key_sound.set_latency_logging(enabled)
}

pub fn key_sound_latency_logging_available(&self) -> bool {
self.key_sound.latency_logging_available()
}

pub fn key_sound_load_soundpack(&self, soundpack_dir: &str) -> Result<KeySoundStatus, String> {
self.key_sound
.load_soundpack_dir(soundpack_dir)
.map_err(|err| err.to_string())
}

pub fn key_sound_unload_soundpack(&self) -> KeySoundStatus {
self.key_sound.unload_soundpack()
}

pub fn key_sound_invalidate_file_cache(&self, path: &str) {
self.key_sound.invalidate_file_cache(path);
}

fn resolve_key_sound_binding(&self, mode: &str, key_label: &str) -> Option<(String, f32)> {
self.store.with_state(|state| {
let mappings = state.keys.get(mode)?;
let positions = state.key_positions.get(mode)?;

for (index, mapped_key) in mappings.iter().enumerate() {
if mapped_key != key_label {
continue;
}

let Some(position) = positions.get(index) else {
continue;
};

if !position.sound_enabled.unwrap_or(false) {
continue;
}
let Some(sound_path) = position.sound_path.as_ref() else {
continue;
};
let trimmed_path = sound_path.trim();
if trimmed_path.is_empty() {
continue;
}

let volume_percent = position.sound_volume.unwrap_or(100.0);
let per_key_volume = (volume_percent / 100.0).clamp(0.0, 1.0) as f32;
return Some((trimmed_path.to_string(), per_key_volume));
}

None
})
}

// ========== CSS 핫리로딩 관련 메서드 ==========

/// CSS 워처 초기화
Expand Down
48 changes: 48 additions & 0 deletions src-tauri/src/commands/key_sound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use tauri::State;

use crate::{app_state::AppState, key_sound::KeySoundStatus};

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_get_status(state: State<'_, AppState>) -> Result<KeySoundStatus, String> {
Ok(state.key_sound_status())
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_set_enabled(
state: State<'_, AppState>,
enabled: bool,
) -> Result<KeySoundStatus, String> {
Ok(state.key_sound_set_enabled(enabled))
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_set_volume(
state: State<'_, AppState>,
volume: f32,
) -> Result<KeySoundStatus, String> {
Ok(state.key_sound_set_volume(volume))
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_load_soundpack(
state: State<'_, AppState>,
soundpack_dir: String,
) -> Result<KeySoundStatus, String> {
state.key_sound_load_soundpack(&soundpack_dir)
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_unload_soundpack(state: State<'_, AppState>) -> Result<KeySoundStatus, String> {
Ok(state.key_sound_unload_soundpack())
}

#[tauri::command(permission = "dmnote-allow-all")]
pub fn key_sound_set_latency_logging(
state: State<'_, AppState>,
enabled: bool,
) -> Result<KeySoundStatus, String> {
if enabled && !state.key_sound_latency_logging_available() {
return Err("Latency measurement is only available in tauri dev/debug builds".to_string());
}
Ok(state.key_sound_set_latency_logging(enabled))
}
2 changes: 2 additions & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ pub mod font;
pub mod graph_items;
pub mod image;
pub mod js;
pub mod key_sound;
pub mod keys;
pub mod overlay;
pub mod plugin_storage;
pub mod preset;
pub mod settings;
pub mod sound;
pub mod stat_items;
pub mod system;
pub mod update;
Loading