Skip to content

Commit 5f1ddf4

Browse files
authored
Merge pull request #101 from supermarsx/smx/rework-audio-engine-play-method-2025-11-02
Rely on voice pooling for audio bursts
2 parents e79d7ac + f81ac70 commit 5f1ddf4

11 files changed

Lines changed: 30 additions & 34 deletions

File tree

agents.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Keys (subset):
136136

137137
- `mute` (bool, default false)
138138

139-
- `sound_cooldown_ms` (int, default 150)
139+
- `sound_cooldown_ms` (deprecated; retained for backwards compatibility)
140140

141141
- `max_concurrent_playbacks` (int, default 16)
142142

@@ -168,7 +168,8 @@ Keys (subset):
168168

169169
- **Keyboard**: physical keydown triggers → enqueue sound (polyphonic) + badge spawn (rate‑limited).
170170

171-
- **Audio**: debounce via `sound_cooldown_ms`; never cut playing voices; LRU drop when > `max_concurrent_playbacks`.
171+
- **Audio**: rely on voice pooling/`max_concurrent_playbacks`; legacy `sound_cooldown_ms` is deprecated. Never cut playing voices beyond
172+
the configured limit (use LRU eviction).
172173

173174
- **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.
174175

lizard.json.sample

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
// Mute all audio output (default: false)
66
"mute": false,
77

8-
// Minimum milliseconds between sound plays (default: 150)
9-
"sound_cooldown_ms": 150,
10-
118
// Maximum simultaneous sound playbacks (default: 16)
129
"max_concurrent_playbacks": 16,
10+
// Legacy setting `sound_cooldown_ms` is deprecated and ignored; remove it
11+
// from existing configs to rely on voice pooling and max_concurrent_playbacks.
1312

1413
// Limit on badge spawns per second and maximum badges visible at once
1514
// (oldest badges are dropped when exceeded, default: 12)

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ Copy this file to `lizard.json` and edit as needed.
9494
Common options include:
9595

9696
- `enabled` and `mute` to toggle overlay and audio
97-
- `sound_cooldown_ms` and `max_concurrent_playbacks` to manage audio bursts
97+
- `max_concurrent_playbacks` to manage audio bursts (legacy `sound_cooldown_ms`
98+
is deprecated and ignored)
9899
- `badges_per_second_max`, `badge_min_px`, `badge_max_px` to tune visuals
99100
- `fullscreen_pause` to suspend in full-screen apps
100101
- `exclude_processes` to ignore specific executables

spec.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs)
4444

4545
* On any keypress (including repeats):
4646

47-
* **Sound**: play once if at least `sound_cooldown_ms` elapsed since last trigger (default 150 ms).
47+
* **Sound**: always trigger playback; bursts are limited by `max_concurrent_playbacks` via voice pooling.
4848
* **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).
4949
* **Badge visuals**:
5050

@@ -91,7 +91,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs)
9191

9292
* **Default**: miniaudio (WASAPI shared), decoded PCM cached in RAM.
9393
* Low latency play calls; allow overlap (polyphony) up to `max_concurrent_playbacks` (default 16).
94-
* **Debounce** using `sound_cooldown_ms`.
94+
* Legacy `sound_cooldown_ms` is deprecated; voice pooling/LRU handles burst control.
9595
* **Volume** 0–100% (default 65%).
9696
* If device changes, auto-reinit.
9797

@@ -107,7 +107,7 @@ Most third-party libs are vendored in `third_party/` as source (no dynamic DLLs)
107107

108108
* `enabled` (bool, default true)
109109
* `mute` (bool, default false)
110-
* `sound_cooldown_ms` (int, default 150)
110+
* `sound_cooldown_ms` (deprecated; retained for backwards compatibility)
111111
* `max_concurrent_playbacks` (int, default 16)
112112
* `badges_per_second_max` (int, default 12)
113113
* `badge_min_px` / `badge_max_px` (int, default 60/108)

src/app/config.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ std::filesystem::path Config::user_config_path() {
9696
void Config::load(std::unique_lock<std::shared_mutex> &lock) {
9797
(void)lock; // lock is held by caller
9898
logging_path_ = config_path_.parent_path() / "lizard.log";
99+
sound_cooldown_ms_ = 0;
99100
std::ifstream in(config_path_);
100101
if (!in.is_open()) {
101102
spdlog::warn("Could not open config file: {}", config_path_.string());
@@ -119,7 +120,13 @@ void Config::load(std::unique_lock<std::shared_mutex> &lock) {
119120
return value;
120121
};
121122

122-
sound_cooldown_ms_ = clamp_nonneg(j.value("sound_cooldown_ms", 150), "sound_cooldown_ms");
123+
if (j.contains("sound_cooldown_ms")) {
124+
int requested = clamp_nonneg(j.value("sound_cooldown_ms", 0), "sound_cooldown_ms");
125+
if (requested > 0) {
126+
spdlog::warn(
127+
"sound_cooldown_ms is deprecated and ignored; bursts are limited by max_concurrent_playbacks");
128+
}
129+
}
123130
max_concurrent_playbacks_ =
124131
clamp_nonneg(j.value("max_concurrent_playbacks", 16), "max_concurrent_playbacks");
125132
badges_per_second_max_ =

src/app/config.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Config {
2727
std::vector<std::string> emoji_pngs() const;
2828
std::optional<std::filesystem::path> sound_path() const;
2929
std::optional<std::filesystem::path> emoji_atlas() const;
30+
// Deprecated: retained for compatibility; always returns 0.
3031
int sound_cooldown_ms() const;
3132
int max_concurrent_playbacks() const;
3233
int badges_per_second_max() const;
@@ -65,7 +66,7 @@ class Config {
6566
// config values
6667
bool enabled_{true};
6768
bool mute_{false};
68-
int sound_cooldown_ms_{150};
69+
int sound_cooldown_ms_{0};
6970
int max_concurrent_playbacks_{16};
7071
int badges_per_second_max_{12};
7172
int badge_min_px_{60};

src/app/main.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ int main(int argc, char **argv) {
5757
: static_cast<std::size_t>(cfg.logging_worker_count());
5858
lizard::util::init_logging(level, queue, workers, cfg.logging_path());
5959

60-
lizard::audio::Engine engine(static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()),
61-
std::chrono::milliseconds(cfg.sound_cooldown_ms()));
60+
lizard::audio::Engine engine(static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()));
6261
engine.init(cfg.sound_path(), cfg.volume_percent(), cfg.audio_backend(),
6362
static_cast<std::uint32_t>(cfg.max_concurrent_playbacks()));
6463

src/audio/engine.cpp

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@ void Engine::endpoint_callback(ma_context *, ma_device_type deviceType,
8888
self->set_volume_locked(currentVol);
8989
}
9090

91-
Engine::Engine(std::uint32_t maxPlaybacks, std::chrono::milliseconds cooldown)
92-
: m_maxPlaybacks(maxPlaybacks), m_cooldown(cooldown) {}
91+
Engine::Engine(std::uint32_t maxPlaybacks) : m_maxPlaybacks(maxPlaybacks) {}
9392

9493
Engine::~Engine() { shutdown(); }
9594

@@ -195,10 +194,6 @@ void Engine::shutdown() {
195194
void Engine::play() {
196195
std::lock_guard<std::mutex> lock(m_mutex);
197196
auto now = std::chrono::steady_clock::now();
198-
if ((now - m_lastPlay) < m_cooldown) {
199-
return;
200-
}
201-
m_lastPlay = now;
202197

203198
Voice *target = nullptr;
204199
for (auto &voice : m_voices) {

src/audio/engine.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ namespace lizard::audio {
2121

2222
class Engine {
2323
public:
24-
Engine(std::uint32_t maxPlaybacks = 16,
25-
std::chrono::milliseconds cooldown = std::chrono::milliseconds(150));
24+
Engine(std::uint32_t maxPlaybacks = 16);
2625
~Engine();
2726

2827
bool init(std::optional<std::filesystem::path> sound_path = std::nullopt,
@@ -47,8 +46,6 @@ class Engine {
4746
ma_audio_buffer m_buffer{};
4847
std::vector<Voice> m_voices;
4948
std::uint32_t m_maxPlaybacks = 0;
50-
std::chrono::steady_clock::time_point m_lastPlay{};
51-
std::chrono::milliseconds m_cooldown{};
5249
float m_volume{1.0f};
5350
std::optional<std::filesystem::path> m_soundPath{};
5451
int m_volumePercent{100};

src/tests/audio_tests.cpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#include <catch2/catch_test_macros.hpp>
22
#include <chrono>
3-
#include <thread>
43

54
int g_start_calls = 0;
65
int g_stop_calls = 0;
@@ -18,8 +17,6 @@ struct AudioTestAccess {
1817
}
1918
};
2019

21-
using namespace std::chrono_literals;
22-
2320
TEST_CASE("max_concurrent_playbacks respected", "[audio]") {
2421
lizard::audio::Engine eng;
2522
AudioTestAccess::voices(eng).resize(16);
@@ -43,18 +40,17 @@ TEST_CASE("max_concurrent_playbacks respected", "[audio]") {
4340
REQUIRE(playing == 16);
4441
}
4542

46-
TEST_CASE("cooldown prevents rapid retriggers", "[audio]") {
47-
lizard::audio::Engine eng(1, 50ms);
43+
TEST_CASE("play retriggers immediately", "[audio]") {
44+
lizard::audio::Engine eng(1);
4845
AudioTestAccess::voices(eng).resize(1);
4946

5047
g_start_calls = 0;
5148
g_stop_calls = 0;
5249

5350
eng.play();
5451
eng.play();
55-
REQUIRE(g_start_calls == 1);
56-
57-
std::this_thread::sleep_for(60ms);
5852
eng.play();
59-
REQUIRE(g_start_calls == 2);
53+
54+
REQUIRE(g_start_calls == 3);
55+
REQUIRE(g_stop_calls == 2);
6056
}

0 commit comments

Comments
 (0)