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
160 changes: 154 additions & 6 deletions projects/APPLaunch/main/ui/launcher_media_controls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,131 @@
#include "model/setup_value_policy.hpp"

#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <thread>

namespace {

LauncherMediaControlsModel model;

/* Volume cached at launcher startup. Every successful write (and every
* authoritative mixer read routed through update_cached_volume()) refreshes
* it, so shortcuts and Settings render one value instead of racing the mixer. */
std::atomic<int> g_cached_volume{-1};

constexpr int kStartupVolumeReadAttempts = 5;
constexpr std::chrono::milliseconds kStartupVolumeReadInterval{400};

int read_mixer_volume()
{
int volume = -1;
try {
cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) {
int parsed = 0;
if (code == 0 && LauncherMediaControlsModel::parse_percent(data, parsed)) volume = parsed;
});
} catch (...) {
return -1;
}
return volume;
}

/* Reads the mixer on a worker thread because a PulseAudio session can take up
* to two seconds to answer, and the startup path must not block the first
* frame. WirePlumber applies the speaker port's stored/default volume while
* the card comes up, which can land after the first successful read, so the
* value is published only once two consecutive reads agree. If that never
* happens, the last valid reading is used rather than leaving the cache empty.
* The worker is joinable: launcher teardown joins it before the audio signal
* is released, and the static destructor joins as a last resort. */
class StartupVolumeCache
{
public:
~StartupVolumeCache() { stop(); }

void start()
{
stop();
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = false;
}
worker_ = std::thread([this] {
try {
run();
} catch (...) {
}
});
}

void stop()
{
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
wake_.notify_all();
if (worker_.joinable())
worker_.join();
}

private:
bool stop_requested()
{
std::lock_guard<std::mutex> lock(mutex_);
return stop_;
}

bool wait_for_next_attempt()
{
std::unique_lock<std::mutex> lock(mutex_);
wake_.wait_for(lock, kStartupVolumeReadInterval, [this] { return stop_; });
return stop_;
}

void publish(int volume)
{
g_cached_volume.store(LauncherMediaControlsModel::clamp_percent(volume));
}

void run()
{
int previous = -1;
int last_valid = -1;
for (int attempt = 0; attempt < kStartupVolumeReadAttempts; ++attempt) {
if (attempt > 0 && wait_for_next_attempt())
return;
if (stop_requested())
return;

const int volume = read_mixer_volume();
if (volume < 0) {
previous = -1;
continue;
}
last_valid = volume;
if (volume == previous) {
publish(volume);
return;
}
previous = volume;
}
if (last_valid >= 0 && !stop_requested())
publish(last_valid);
}

std::mutex mutex_;
std::condition_variable wake_;
bool stop_ = false;
std::thread worker_;
};

StartupVolumeCache g_startup_volume_cache;

int read_config_int(const char *key, int fallback)
{
int value = fallback;
Expand Down Expand Up @@ -49,14 +168,17 @@ bool write_config_int(const char *key, int value, int fallback)

int read_volume()
{
int volume = -1;
cp0_signal_audio_api({"VolumeRead"}, [&](int code, std::string data) {
int parsed = 0;
if (code == 0 && LauncherMediaControlsModel::parse_percent(data, parsed)) volume = parsed;
});
const int cached = g_cached_volume.load();
if (cached >= 0) return LauncherMediaControlsModel::clamp_percent(cached);

int volume = read_mixer_volume();
if (volume < 0)
volume = read_config_int("volume", model.volume_or(50));
return LauncherMediaControlsModel::clamp_percent(volume);
volume = LauncherMediaControlsModel::clamp_percent(volume);
/* The first authoritative value becomes the cache, so a shortcut pressed
* before the startup read completed cannot disagree with Settings. */
g_cached_volume.store(volume);
return volume;
}

bool read_mute()
Expand Down Expand Up @@ -87,6 +209,7 @@ int write_volume(int previous, int percent)
return previous;
}
model.set_volume(written);
g_cached_volume.store(written);
return written;
}

Expand Down Expand Up @@ -161,6 +284,31 @@ int write_brightness(int previous_percent, int percent)

namespace launcher_media_controls {

bool has_cached_volume()
{
return g_cached_volume.load() >= 0;
}

int cached_volume()
{
return g_cached_volume.load();
}

void start_volume_cache()
{
g_startup_volume_cache.start();
}

void stop_volume_cache()
{
g_startup_volume_cache.stop();
}

void update_cached_volume(int percent)
{
g_cached_volume.store(LauncherMediaControlsModel::clamp_percent(percent));
}

int adjust_volume(int delta_percent)
{
if (read_mute())
Expand Down
15 changes: 15 additions & 0 deletions projects/APPLaunch/main/ui/launcher_media_controls.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ namespace launcher_media_controls {
constexpr int VOLUME_STEP_PERCENT =
setup_values::volume_metric(setup_values::VolumeMetric::StepPercent);

/* Volume cached for the lifetime of the launcher process. The launcher reads
* the system mixer once at startup and keeps this value in sync with every
* volume write it performs, so the shortcut OSD and Settings -> Speaker render
* the same number instead of querying the mixer at different times.
* cached_volume() returns -1 until the startup read produced a value. */
bool has_cached_volume();
int cached_volume();
/* Read the mixer once, off the LVGL thread, and publish the settled value.
* Safe to call once at launcher startup; a later call restarts the read. */
void start_volume_cache();
/* Join the startup reader so teardown cannot race a signal call. */
void stop_volume_cache();
/* Record a value that was just written to the mixer, or read back from it. */
void update_cached_volume(int percent);

int adjust_volume(int delta_percent);
int adjust_brightness(int delta_percent);
bool toggle_mute();
Expand Down
32 changes: 22 additions & 10 deletions projects/APPLaunch/main/ui/settings/settings_volume_page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "settings_volume_page.hpp"

#include "hal_lvgl_bsp.h"
#include "../launcher_media_controls.h"

#include <charconv>
#include <chrono>
Expand Down Expand Up @@ -158,9 +159,10 @@ class settings_audio_com {
return saved.code == 0 ? saved : backend;
}

// The persisted key is what the startup restore writes to the mixer, so it
// is the best value the page can show before the asynchronous mixer read
// returns. A missing or invalid entry keeps the supplied fallback.
// The persisted key is only a fallback: the launcher caches the mixer
// volume at startup, and that cache is what the shortcut OSD and this page
// must both render. A page opened before the startup read completed keeps
// the persisted value and the asynchronous read below refreshes both.
static int read_saved_volume(int fallback)
{
const auto saved = decode_volume(invoke_config_default(
Expand Down Expand Up @@ -310,11 +312,13 @@ void LvSettingVolumePage3::initialize_page(lv_obj_t *parent, std::function<void(
{
back_callback_ = std::move(back_callback);
LeaveSelfPage = [this] { request_back(); };
// The mixer read runs off the LVGL thread, so creating the list with the
// default selection would show 100% until the result arrives. Seed the
// page with the persisted volume first; every commit saves that key and
// the startup restore writes it back to the mixer.
const int saved = settings_audio_com::read_saved_volume(metric(LayoutMetric::MaxVolume));
// The first frame must show the volume the launcher cached at startup, so
// Settings -> Speaker and the shortcut OSD agree. The persisted key is
// only used when the startup read has not produced a value yet; the
// asynchronous mixer read below refreshes both when it returns.
const int saved = launcher_media_controls::has_cached_volume()
? launcher_media_controls::cached_volume()
: settings_audio_com::read_saved_volume(metric(LayoutMetric::MaxVolume));
original_volume_ = saved;
backend_volume_ = saved;
last_preview_requested_ = saved;
Expand Down Expand Up @@ -532,6 +536,9 @@ void LvSettingVolumePage3::handle_volume_result(const ActiveRequest &request,
const bool valid = result.code == 0 && settings_audio_com::volume_value_valid(result.value);
if (request.kind == RequestKind::Read) {
if (valid) {
/* The mixer is authoritative; publish what it reports so the
* shortcut OSD and every later render share this value. */
launcher_media_controls::update_cached_volume(result.value);
original_volume_ = result.value;
backend_volume_ = result.value;
last_preview_requested_ = result.value;
Expand All @@ -554,10 +561,12 @@ void LvSettingVolumePage3::handle_volume_result(const ActiveRequest &request,
rollback_pending_ = false;
commit_requested_ = false;
commit_leave_requested_ = false;
if (valid)
if (valid) {
backend_volume_ = result.value;
else
launcher_media_controls::update_cached_volume(result.value);
} else {
last_error_ = result.data.empty() ? "unable to restore volume" : result.data;
}
select_volume(selection_for_volume(original_volume_));
preview_touched_ = false;
last_preview_requested_ = backend_volume_;
Expand All @@ -574,6 +583,9 @@ void LvSettingVolumePage3::handle_volume_result(const ActiveRequest &request,
}

backend_volume_ = result.value;
/* Preview and commit writes already changed the mixer; keep the shared
* cache in step so leaving this page cannot expose a stale value. */
launcher_media_controls::update_cached_volume(result.value);
last_error_.clear();
if (request.kind == RequestKind::PreviewWrite) {
handle_preview_success(request);
Expand Down
5 changes: 5 additions & 0 deletions projects/APPLaunch/main/ui/ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// Project name: zero

#include "ui.h"
#include "launcher_media_controls.h"
#include "launcher_ui_app_page.hpp"
#include "ui_low_battery.h"
#include "ui_global_hint.h"
Expand Down Expand Up @@ -37,6 +38,9 @@ void init()
{
if (home)
return;
/* Cache the system volume once here so the shortcut OSD and
* Settings -> Speaker share one value for the whole launcher session. */
launcher_media_controls::start_volume_cache();
cp0_keyboard_set_global_key_handler(ui_global_hint::on_key);
home = std::make_unique<LauncherUiRuntime>();
home->start();
Expand All @@ -46,6 +50,7 @@ void init()
void deinit()
{
cp0_keyboard_set_global_key_handler(nullptr);
launcher_media_controls::stop_volume_cache();
ui_global_hint::shutdown();
launcher_battery_ui::shutdown_warning();
ui_loading::shutdown();
Expand Down
2 changes: 1 addition & 1 deletion projects/APPLaunch/tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ ${CXX:-g++} -std=c++17 -Wall -Wextra -Werror \
-o "$build_dir/test_launcher_media_model"
"$build_dir/test_launcher_media_model"

${CXX:-g++} -std=c++17 -Wall -Wextra -Werror \
${CXX:-g++} -std=c++17 -Wall -Wextra -Werror -pthread \
-I"$(dirname "$0")/../../../ext_components/cp0_lvgl/include" \
-I"$(dirname "$0")/../../../SDK/github_source/eventpp/include" \
"$(dirname "$0")/test_launcher_media_controls.cpp" \
Expand Down
Loading
Loading