diff --git a/agents.md b/agents.md index 391b4f2..edfde78 100644 --- a/agents.md +++ b/agents.md @@ -136,7 +136,7 @@ Keys (subset): - `mute` (bool, default false) -- `sound_cooldown_ms` (int, default 150) +- `sound_cooldown_ms` (deprecated; retained for backwards compatibility) - `max_concurrent_playbacks` (int, default 16) @@ -168,7 +168,8 @@ Keys (subset): - **Keyboard**: physical keydown triggers → enqueue sound (polyphonic) + badge spawn (rate‑limited). -- **Audio**: debounce via `sound_cooldown_ms`; never cut playing voices; LRU drop when > `max_concurrent_playbacks`. +- **Audio**: rely on voice pooling/`max_concurrent_playbacks`; legacy `sound_cooldown_ms` is deprecated. Never cut playing voices beyond + the configured limit (use LRU eviction). - **Overlay**: click‑through, topmost, no focus change; GL render loop targets display refresh (auto detect; fallback 60 FPS). Back‑pressure if active badges >150; resume at <80. diff --git a/lizard.json.sample b/lizard.json.sample index 46c6e12..45e409c 100644 --- a/lizard.json.sample +++ b/lizard.json.sample @@ -5,11 +5,10 @@ // Mute all audio output (default: false) "mute": false, - // Minimum milliseconds between sound plays (default: 150) - "sound_cooldown_ms": 150, - // Maximum simultaneous sound playbacks (default: 16) "max_concurrent_playbacks": 16, + // Legacy setting `sound_cooldown_ms` is deprecated and ignored; remove it + // from existing configs to rely on voice pooling and max_concurrent_playbacks. // Limit on badge spawns per second and maximum badges visible at once // (oldest badges are dropped when exceeded, default: 12) diff --git a/readme.md b/readme.md index ebf05f5..4597a13 100644 --- a/readme.md +++ b/readme.md @@ -94,7 +94,8 @@ Copy this file to `lizard.json` and edit as needed. Common options include: - `enabled` and `mute` to toggle overlay and audio -- `sound_cooldown_ms` and `max_concurrent_playbacks` to manage audio bursts +- `max_concurrent_playbacks` to manage audio bursts (legacy `sound_cooldown_ms` + is deprecated and ignored) - `badges_per_second_max`, `badge_min_px`, `badge_max_px` to tune visuals - `fullscreen_pause` to suspend in full-screen apps - `exclude_processes` to ignore specific executables diff --git a/spec.md b/spec.md index e22074b..211d91c 100644 --- a/spec.md +++ b/spec.md @@ -44,7 +44,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs) * On any keypress (including repeats): - * **Sound**: play once if at least `sound_cooldown_ms` elapsed since last trigger (default 150 ms). + * **Sound**: always trigger playback; bursts are limited by `max_concurrent_playbacks` via voice pooling. * **Badge**: spawn 1 circular badge at a random screen location (or near caret if available; see §11), limited by `badges_per_second_max` (default 12). * **Badge visuals**: @@ -91,7 +91,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs) * **Default**: miniaudio (WASAPI shared), decoded PCM cached in RAM. * Low latency play calls; allow overlap (polyphony) up to `max_concurrent_playbacks` (default 16). -* **Debounce** using `sound_cooldown_ms`. +* Legacy `sound_cooldown_ms` is deprecated; voice pooling/LRU handles burst control. * **Volume** 0–100% (default 65%). * If device changes, auto-reinit. @@ -107,7 +107,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs) * `enabled` (bool, default true) * `mute` (bool, default false) - * `sound_cooldown_ms` (int, default 150) + * `sound_cooldown_ms` (deprecated; retained for backwards compatibility) * `max_concurrent_playbacks` (int, default 16) * `badges_per_second_max` (int, default 12) * `badge_min_px` / `badge_max_px` (int, default 60/108) diff --git a/src/app/config.cpp b/src/app/config.cpp index 0bf147d..f6705eb 100644 --- a/src/app/config.cpp +++ b/src/app/config.cpp @@ -96,6 +96,7 @@ std::filesystem::path Config::user_config_path() { void Config::load(std::unique_lock &lock) { (void)lock; // lock is held by caller logging_path_ = config_path_.parent_path() / "lizard.log"; + sound_cooldown_ms_ = 0; std::ifstream in(config_path_); if (!in.is_open()) { spdlog::warn("Could not open config file: {}", config_path_.string()); @@ -119,7 +120,13 @@ void Config::load(std::unique_lock &lock) { return value; }; - sound_cooldown_ms_ = clamp_nonneg(j.value("sound_cooldown_ms", 150), "sound_cooldown_ms"); + if (j.contains("sound_cooldown_ms")) { + int requested = clamp_nonneg(j.value("sound_cooldown_ms", 0), "sound_cooldown_ms"); + if (requested > 0) { + spdlog::warn( + "sound_cooldown_ms is deprecated and ignored; bursts are limited by max_concurrent_playbacks"); + } + } max_concurrent_playbacks_ = clamp_nonneg(j.value("max_concurrent_playbacks", 16), "max_concurrent_playbacks"); badges_per_second_max_ = diff --git a/src/app/config.h b/src/app/config.h index 12f22a5..69b0fce 100644 --- a/src/app/config.h +++ b/src/app/config.h @@ -27,6 +27,7 @@ class Config { std::vector emoji_pngs() const; std::optional sound_path() const; std::optional emoji_atlas() const; + // Deprecated: retained for compatibility; always returns 0. int sound_cooldown_ms() const; int max_concurrent_playbacks() const; int badges_per_second_max() const; @@ -65,7 +66,7 @@ class Config { // config values bool enabled_{true}; bool mute_{false}; - int sound_cooldown_ms_{150}; + int sound_cooldown_ms_{0}; int max_concurrent_playbacks_{16}; int badges_per_second_max_{12}; int badge_min_px_{60}; diff --git a/src/app/main.cpp b/src/app/main.cpp index 12ad65a..ba90654 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -57,8 +57,7 @@ int main(int argc, char **argv) { : static_cast(cfg.logging_worker_count()); lizard::util::init_logging(level, queue, workers, cfg.logging_path()); - lizard::audio::Engine engine(static_cast(cfg.max_concurrent_playbacks()), - std::chrono::milliseconds(cfg.sound_cooldown_ms())); + lizard::audio::Engine engine(static_cast(cfg.max_concurrent_playbacks())); engine.init(cfg.sound_path(), cfg.volume_percent(), cfg.audio_backend(), static_cast(cfg.max_concurrent_playbacks())); diff --git a/src/audio/engine.cpp b/src/audio/engine.cpp index f458fa1..54debb0 100644 --- a/src/audio/engine.cpp +++ b/src/audio/engine.cpp @@ -88,8 +88,7 @@ void Engine::endpoint_callback(ma_context *, ma_device_type deviceType, self->set_volume_locked(currentVol); } -Engine::Engine(std::uint32_t maxPlaybacks, std::chrono::milliseconds cooldown) - : m_maxPlaybacks(maxPlaybacks), m_cooldown(cooldown) {} +Engine::Engine(std::uint32_t maxPlaybacks) : m_maxPlaybacks(maxPlaybacks) {} Engine::~Engine() { shutdown(); } @@ -195,10 +194,6 @@ void Engine::shutdown() { void Engine::play() { std::lock_guard lock(m_mutex); auto now = std::chrono::steady_clock::now(); - if ((now - m_lastPlay) < m_cooldown) { - return; - } - m_lastPlay = now; Voice *target = nullptr; for (auto &voice : m_voices) { diff --git a/src/audio/engine.h b/src/audio/engine.h index c936936..3730bed 100644 --- a/src/audio/engine.h +++ b/src/audio/engine.h @@ -21,8 +21,7 @@ namespace lizard::audio { class Engine { public: - Engine(std::uint32_t maxPlaybacks = 16, - std::chrono::milliseconds cooldown = std::chrono::milliseconds(150)); + Engine(std::uint32_t maxPlaybacks = 16); ~Engine(); bool init(std::optional sound_path = std::nullopt, @@ -47,8 +46,6 @@ class Engine { ma_audio_buffer m_buffer{}; std::vector m_voices; std::uint32_t m_maxPlaybacks = 0; - std::chrono::steady_clock::time_point m_lastPlay{}; - std::chrono::milliseconds m_cooldown{}; float m_volume{1.0f}; std::optional m_soundPath{}; int m_volumePercent{100}; diff --git a/src/tests/audio_tests.cpp b/src/tests/audio_tests.cpp index e845670..38cbbb5 100644 --- a/src/tests/audio_tests.cpp +++ b/src/tests/audio_tests.cpp @@ -1,6 +1,5 @@ #include #include -#include int g_start_calls = 0; int g_stop_calls = 0; @@ -18,8 +17,6 @@ struct AudioTestAccess { } }; -using namespace std::chrono_literals; - TEST_CASE("max_concurrent_playbacks respected", "[audio]") { lizard::audio::Engine eng; AudioTestAccess::voices(eng).resize(16); @@ -43,8 +40,8 @@ TEST_CASE("max_concurrent_playbacks respected", "[audio]") { REQUIRE(playing == 16); } -TEST_CASE("cooldown prevents rapid retriggers", "[audio]") { - lizard::audio::Engine eng(1, 50ms); +TEST_CASE("play retriggers immediately", "[audio]") { + lizard::audio::Engine eng(1); AudioTestAccess::voices(eng).resize(1); g_start_calls = 0; @@ -52,9 +49,8 @@ TEST_CASE("cooldown prevents rapid retriggers", "[audio]") { eng.play(); eng.play(); - REQUIRE(g_start_calls == 1); - - std::this_thread::sleep_for(60ms); eng.play(); - REQUIRE(g_start_calls == 2); + + REQUIRE(g_start_calls == 3); + REQUIRE(g_stop_calls == 2); } diff --git a/src/tests/config_tests.cpp b/src/tests/config_tests.cpp index 123f0c7..e7d8b9d 100644 --- a/src/tests/config_tests.cpp +++ b/src/tests/config_tests.cpp @@ -208,7 +208,7 @@ TEST_CASE("logs warnings for adjusted values", "[config]") { auto tempdir = std::filesystem::temp_directory_path(); auto cfg_file = tempdir / "lizard_cfg_warn.json"; std::ofstream out(cfg_file); - out << R"({"sound_cooldown_ms":-5,"badge_min_px":-2,"badge_max_px":-1,"volume_percent":150})"; + out << R"({"sound_cooldown_ms":150,"badge_min_px":-2,"badge_max_px":-1,"volume_percent":150})"; out.close(); auto sink = std::make_shared(32); @@ -222,7 +222,7 @@ TEST_CASE("logs warnings for adjusted values", "[config]") { bool saw_badge_max = false; bool saw_volume = false; for (const auto &line : sink->last_formatted()) { - if (line.find("sound_cooldown_ms") != std::string::npos) + if (line.find("sound_cooldown_ms is deprecated") != std::string::npos) saw_sound = true; if (line.find("badge_min_px") != std::string::npos) saw_badge_min = true;