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
5 changes: 3 additions & 2 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.

Expand Down
5 changes: 2 additions & 3 deletions lizard.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand Down Expand Up @@ -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.

Expand All @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion src/app/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ std::filesystem::path Config::user_config_path() {
void Config::load(std::unique_lock<std::shared_mutex> &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());
Expand All @@ -119,7 +120,13 @@ void Config::load(std::unique_lock<std::shared_mutex> &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_ =
Expand Down
3 changes: 2 additions & 1 deletion src/app/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Config {
std::vector<std::string> emoji_pngs() const;
std::optional<std::filesystem::path> sound_path() const;
std::optional<std::filesystem::path> 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;
Expand Down Expand Up @@ -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};
Expand Down
3 changes: 1 addition & 2 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ int main(int argc, char **argv) {
: static_cast<std::size_t>(cfg.logging_worker_count());
lizard::util::init_logging(level, queue, workers, cfg.logging_path());

lizard::audio::Engine engine(static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()),
std::chrono::milliseconds(cfg.sound_cooldown_ms()));
lizard::audio::Engine engine(static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()));
engine.init(cfg.sound_path(), cfg.volume_percent(), cfg.audio_backend(),
static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()));

Expand Down
7 changes: 1 addition & 6 deletions src/audio/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }

Expand Down Expand Up @@ -195,10 +194,6 @@ void Engine::shutdown() {
void Engine::play() {
std::lock_guard<std::mutex> 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) {
Expand Down
5 changes: 1 addition & 4 deletions src/audio/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::filesystem::path> sound_path = std::nullopt,
Expand All @@ -47,8 +46,6 @@ class Engine {
ma_audio_buffer m_buffer{};
std::vector<Voice> 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<std::filesystem::path> m_soundPath{};
int m_volumePercent{100};
Expand Down
14 changes: 5 additions & 9 deletions src/tests/audio_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <thread>

int g_start_calls = 0;
int g_stop_calls = 0;
Expand All @@ -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);
Expand All @@ -43,18 +40,17 @@ 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;
g_stop_calls = 0;

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);
}
4 changes: 2 additions & 2 deletions src/tests/config_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<spdlog::sinks::ringbuffer_sink_mt>(32);
Expand All @@ -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;
Expand Down
Loading