Audio System + Drivers (#562)

This commit is contained in:
Shadowtrance
2026-07-14 16:34:29 +10:00
committed by GitHub
parent fa4a6e255c
commit 955416dac8
137 changed files with 8847 additions and 387 deletions
@@ -0,0 +1,48 @@
#pragma once
#include <Tactility/PubSub.h>
#include <memory>
namespace tt::service::audio {
enum class AudioEvent {
InputEnabledChanged,
OutputEnabledChanged,
InputVolumeChanged,
OutputVolumeChanged,
InputMuteChanged,
OutputMuteChanged
};
/** @return the audio pubsub that broadcasts AudioEvent objects */
std::shared_ptr<PubSub<AudioEvent>> getPubsub();
/** @return true when an AUDIO_STREAM_TYPE device is bound and ready */
bool isAvailable();
/** @return true if a codec supporting input (microphone) is bound, e.g. a device with a speaker but no mic returns false */
bool isInputAvailable();
/** @return true if a codec supporting output (speaker) is bound, e.g. a device with a mic but no speaker returns false */
bool isOutputAvailable();
bool isInputEnabled();
void setInputEnabled(bool enabled);
bool isOutputEnabled();
void setOutputEnabled(bool enabled);
float getInputVolume();
void setInputVolume(float percent);
float getOutputVolume();
void setOutputVolume(float percent);
bool isInputMuted();
void setInputMuted(bool muted);
bool isOutputMuted();
void setOutputMuted(bool muted);
} // namespace tt::service::audio
@@ -0,0 +1,68 @@
#pragma once
#include <Tactility/Mutex.h>
#include <Tactility/PubSub.h>
#include <Tactility/Timer.h>
#include <Tactility/service/Service.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/audio/Audio.h>
#include <tactility/drivers/audio_stream.h>
#include <memory>
struct Device;
namespace tt::service::audio {
class AudioService final : public Service {
Mutex mutex;
Device* streamDevice = nullptr;
std::shared_ptr<PubSub<AudioEvent>> pubsub = std::make_shared<PubSub<AudioEvent>>();
std::unique_ptr<Timer> persistTimer;
bool persistPending = false;
void findStreamDevice();
void primeFromSettings() const;
void schedulePersist();
void persistIfPending();
void onDriverChanged(enum AudioCodecDirection direction, enum AudioStreamChange change);
static void onDriverChangedCallback(Device* device, enum AudioCodecDirection direction, enum AudioStreamChange change, void* userData);
public:
bool onStart(ServiceContext& serviceContext) override;
void onStop(ServiceContext& serviceContext) override;
bool isAvailable() const;
/** @return true if a codec supporting input (microphone) is bound for this device */
bool isInputAvailable() const;
/** @return true if a codec supporting output (speaker) is bound for this device */
bool isOutputAvailable() const;
bool isInputEnabled() const;
void setInputEnabled(bool enabled);
bool isOutputEnabled() const;
void setOutputEnabled(bool enabled);
float getInputVolume() const;
void setInputVolume(float percent);
float getOutputVolume() const;
void setOutputVolume(float percent);
bool isInputMuted() const;
void setInputMuted(bool muted);
bool isOutputMuted() const;
void setOutputMuted(bool muted);
std::shared_ptr<PubSub<AudioEvent>> getPubsub() const { return pubsub; }
};
std::shared_ptr<AudioService> findAudioService();
} // namespace tt::service::audio
@@ -0,0 +1,22 @@
#pragma once
namespace tt::settings::audio {
struct AudioSettings {
bool inputEnabled;
bool outputEnabled;
bool inputMuted;
bool outputMuted;
float inputVolume; // 0..100
float outputVolume; // 0..100
};
bool load(AudioSettings& settings);
AudioSettings loadOrGetDefault();
AudioSettings getDefault();
bool save(const AudioSettings& settings);
} // namespace tt::settings::audio
+10
View File
@@ -19,10 +19,12 @@
#include <Tactility/network/NtpPrivate.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/audio/Audio.h>
#include <Tactility/settings/TimePrivate.h>
#include <tactility/concurrent/thread.h>
#include <tactility/crypt_module.h>
#include <tactility/drivers/audio_stream.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/grove.h>
#include <tactility/drivers/power_supply.h>
@@ -72,6 +74,7 @@ bool MainDispatcher::dispatch(Function function, TickType_t timeout) const {
// region Default services
namespace service {
// Primary
namespace audio { extern const ServiceManifest manifest; }
namespace gps { extern const ServiceManifest manifest; }
namespace wifi { extern const ServiceManifest manifest; }
#ifdef ESP_PLATFORM
@@ -111,6 +114,7 @@ namespace app {
namespace appdetails { extern const AppManifest manifest; }
namespace applist { extern const AppManifest manifest; }
namespace appsettings { extern const AppManifest manifest; }
namespace audiosettings { extern const AppManifest manifest; }
namespace boot { extern const AppManifest manifest; }
namespace development { extern const AppManifest manifest; }
namespace display { extern const AppManifest manifest; }
@@ -172,6 +176,9 @@ static void registerInternalApps() {
addAppManifest(app::apphubdetails::manifest);
addAppManifest(app::applist::manifest);
addAppManifest(app::appsettings::manifest);
if (service::audio::isAvailable()) {
addAppManifest(app::audiosettings::manifest);
}
if (device_exists_of_type(&DISPLAY_TYPE)) {
addAppManifest(app::kerneldisplay::manifest);
} else if (hal::hasDevice(hal::Device::Type::Display)) {
@@ -308,6 +315,9 @@ static void registerAndStartSecondaryServices() {
static void registerAndStartPrimaryServices() {
LOG_I(TAG, "Registering and starting primary system services");
if (device_exists_of_type(&AUDIO_STREAM_TYPE)) {
addService(service::audio::manifest);
}
addService(service::gps::manifest);
addService(service::wifi::manifest);
#ifdef ESP_PLATFORM
@@ -0,0 +1,220 @@
#include <Tactility/Tactility.h>
#include <tactility/lvgl_icon_shared.h>
#include <Tactility/PubSub.h>
#include <Tactility/app/App.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/SliderBox.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/audio/Audio.h>
#include <lvgl.h>
#include <tactility/lvgl_module.h>
namespace tt::app::audiosettings {
class AudioSettingsApp final : public App {
PubSub<service::audio::AudioEvent>::SubscriptionHandle audioSubscription = nullptr;
lv_obj_t* inputEnabledSwitch = nullptr;
lv_obj_t* inputMuteSwitch = nullptr;
lv_obj_t* inputVolumeSlider = nullptr;
lv_obj_t* outputEnabledSwitch = nullptr;
lv_obj_t* outputMuteSwitch = nullptr;
lv_obj_t* outputVolumeSlider = nullptr;
static void onInputEnabledSwitch(lv_event_t* event) {
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
service::audio::setInputEnabled(enabled);
}
static void onOutputEnabledSwitch(lv_event_t* event) {
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
service::audio::setOutputEnabled(enabled);
}
static void onInputMuteSwitch(lv_event_t* event) {
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool muted = lv_obj_has_state(sw, LV_STATE_CHECKED);
service::audio::setInputMuted(muted);
}
static void onOutputMuteSwitch(lv_event_t* event) {
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
bool muted = lv_obj_has_state(sw, LV_STATE_CHECKED);
service::audio::setOutputMuted(muted);
}
static void onInputVolumeSlider(lv_event_t* event) {
auto* sliderBox = static_cast<lv_obj_t*>(lv_event_get_target(event));
float percent = static_cast<float>(lvgl::sliderbox_get_value(sliderBox));
service::audio::setInputVolume(percent);
}
static void onOutputVolumeSlider(lv_event_t* event) {
auto* sliderBox = static_cast<lv_obj_t*>(lv_event_get_target(event));
float percent = static_cast<float>(lvgl::sliderbox_get_value(sliderBox));
service::audio::setOutputVolume(percent);
}
static lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
auto* wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_hor(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
auto* title_label = lv_label_create(wrapper);
lv_label_set_text(title_label, title);
return wrapper;
}
static lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb, void* userData) {
auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* row_label = lv_label_create(row);
lv_label_set_text(row_label, label);
lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* sw = lv_switch_create(row);
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, userData);
return sw;
}
static lv_obj_t* createSliderRow(lv_obj_t* parent, const char* label, int32_t initialValue, lv_event_cb_t cb, void* userData) {
auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* row_label = lv_label_create(row);
lv_label_set_text(row_label, label);
lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* sliderBox = lvgl::sliderbox_create(row, 0, 100, 10, initialValue);
lv_obj_set_width(sliderBox, LV_PCT(50));
lv_obj_align(sliderBox, LV_ALIGN_RIGHT_MID, 0, 0);
lvgl::sliderbox_add_value_changed_cb(sliderBox, cb, userData);
return sliderBox;
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
lvgl::toolbar_create(parent, app);
auto* main_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
if (!service::audio::isAvailable()) {
auto* label = lv_label_create(main_wrapper);
lv_label_set_text(label, "No audio hardware available");
lv_obj_center(label);
return;
}
// Gated per-direction, not just isAvailable() - a mic-only or speaker-only
// device (e.g. a dedicated input codec with no output codec bound) should
// only show the section it actually has, not a dead section for the other.
if (service::audio::isInputAvailable()) {
auto* input_section = createSection(main_wrapper, "Microphone");
inputEnabledSwitch = createSwitchRow(input_section, "Enabled", onInputEnabledSwitch, this);
inputMuteSwitch = createSwitchRow(input_section, "Mute", onInputMuteSwitch, this);
inputVolumeSlider = createSliderRow(input_section, "Volume", static_cast<int32_t>(service::audio::getInputVolume()), onInputVolumeSlider, this);
}
if (service::audio::isOutputAvailable()) {
auto* output_section = createSection(main_wrapper, "Speaker");
outputEnabledSwitch = createSwitchRow(output_section, "Enabled", onOutputEnabledSwitch, this);
outputMuteSwitch = createSwitchRow(output_section, "Mute", onOutputMuteSwitch, this);
outputVolumeSlider = createSliderRow(output_section, "Volume", static_cast<int32_t>(service::audio::getOutputVolume()), onOutputVolumeSlider, this);
}
// isAvailable() only reflects that the audio-stream device exists, not that any
// codec is actually bound to it (the stream device is constructed unconditionally
// at module-start time, before devicetree codecs exist, and binds lazily on first
// use) -- so a board with no input or output codec at all reaches here with both
// sections skipped above and would otherwise show an empty page.
if (!service::audio::isInputAvailable() && !service::audio::isOutputAvailable()) {
auto* label = lv_label_create(main_wrapper);
lv_label_set_text(label, "No supported audio controls");
lv_obj_center(label);
}
refresh();
audioSubscription = service::audio::getPubsub()->subscribe([this](auto) {
if (lvgl::lock(lvgl::defaultLockTime)) {
refresh();
lvgl::unlock();
}
});
}
void onHide(AppContext& app) override {
if (audioSubscription != nullptr) {
service::audio::getPubsub()->unsubscribe(audioSubscription);
audioSubscription = nullptr;
}
inputEnabledSwitch = nullptr;
inputMuteSwitch = nullptr;
inputVolumeSlider = nullptr;
outputEnabledSwitch = nullptr;
outputMuteSwitch = nullptr;
outputVolumeSlider = nullptr;
}
void refresh() const {
if (inputEnabledSwitch) {
if (service::audio::isInputEnabled()) lv_obj_add_state(inputEnabledSwitch, LV_STATE_CHECKED);
else lv_obj_remove_state(inputEnabledSwitch, LV_STATE_CHECKED);
}
if (inputMuteSwitch) {
if (service::audio::isInputMuted()) lv_obj_add_state(inputMuteSwitch, LV_STATE_CHECKED);
else lv_obj_remove_state(inputMuteSwitch, LV_STATE_CHECKED);
}
if (inputVolumeSlider) {
lvgl::sliderbox_set_value(inputVolumeSlider, static_cast<int32_t>(service::audio::getInputVolume()), LV_ANIM_OFF);
}
if (outputEnabledSwitch) {
if (service::audio::isOutputEnabled()) lv_obj_add_state(outputEnabledSwitch, LV_STATE_CHECKED);
else lv_obj_remove_state(outputEnabledSwitch, LV_STATE_CHECKED);
}
if (outputMuteSwitch) {
if (service::audio::isOutputMuted()) lv_obj_add_state(outputMuteSwitch, LV_STATE_CHECKED);
else lv_obj_remove_state(outputMuteSwitch, LV_STATE_CHECKED);
}
if (outputVolumeSlider) {
lvgl::sliderbox_set_value(outputVolumeSlider, static_cast<int32_t>(service::audio::getOutputVolume()), LV_ANIM_OFF);
}
}
};
extern const AppManifest manifest = {
.appId = "AudioSettings",
.appName = "Audio",
.appIcon = LVGL_ICON_SHARED_MUSIC_NOTE,
.appCategory = Category::Settings,
.createApp = create<AudioSettingsApp>
};
} // namespace tt::app::audiosettings
+107
View File
@@ -0,0 +1,107 @@
#include <Tactility/service/audio/Audio.h>
#include <Tactility/service/audio/AudioService.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
namespace tt::service::audio {
extern const ServiceManifest manifest;
// The service is only registered when audio hardware is present (see Tactility.cpp);
// treat "not registered" as "no device bound" rather than asserting via findAudioService().
static std::shared_ptr<AudioService> tryFindAudioService() {
return findServiceById<AudioService>(manifest.id);
}
std::shared_ptr<PubSub<AudioEvent>> getPubsub() {
if (auto service = tryFindAudioService()) {
return service->getPubsub();
}
// No audio hardware, service was never registered: hand back an inert pubsub
// that nothing ever publishes to, so callers can subscribe/unsubscribe safely.
static auto fallbackPubsub = std::make_shared<PubSub<AudioEvent>>();
return fallbackPubsub;
}
bool isAvailable() {
auto service = tryFindAudioService();
return service != nullptr && service->isAvailable();
}
bool isInputAvailable() {
auto service = tryFindAudioService();
return service != nullptr && service->isInputAvailable();
}
bool isOutputAvailable() {
auto service = tryFindAudioService();
return service != nullptr && service->isOutputAvailable();
}
bool isInputEnabled() {
auto service = tryFindAudioService();
return service != nullptr && service->isInputEnabled();
}
void setInputEnabled(bool enabled) {
if (auto service = tryFindAudioService()) {
service->setInputEnabled(enabled);
}
}
bool isOutputEnabled() {
auto service = tryFindAudioService();
return service != nullptr && service->isOutputEnabled();
}
void setOutputEnabled(bool enabled) {
if (auto service = tryFindAudioService()) {
service->setOutputEnabled(enabled);
}
}
float getInputVolume() {
auto service = tryFindAudioService();
return service != nullptr ? service->getInputVolume() : 0.0f;
}
void setInputVolume(float percent) {
if (auto service = tryFindAudioService()) {
service->setInputVolume(percent);
}
}
float getOutputVolume() {
auto service = tryFindAudioService();
return service != nullptr ? service->getOutputVolume() : 0.0f;
}
void setOutputVolume(float percent) {
if (auto service = tryFindAudioService()) {
service->setOutputVolume(percent);
}
}
bool isInputMuted() {
auto service = tryFindAudioService();
return service != nullptr && service->isInputMuted();
}
void setInputMuted(bool muted) {
if (auto service = tryFindAudioService()) {
service->setInputMuted(muted);
}
}
bool isOutputMuted() {
auto service = tryFindAudioService();
return service != nullptr && service->isOutputMuted();
}
void setOutputMuted(bool muted) {
if (auto service = tryFindAudioService()) {
service->setOutputMuted(muted);
}
}
} // namespace tt::service::audio
@@ -0,0 +1,336 @@
#include <Tactility/service/audio/AudioService.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/AudioSettings.h>
#include <tactility/device.h>
#include <tactility/drivers/audio_codec.h>
#include <tactility/drivers/audio_stream.h>
#include <tactility/log.h>
namespace tt::service::audio {
constexpr auto* TAG = "AudioService";
extern const ServiceManifest manifest;
constexpr TickType_t PERSIST_INTERVAL_TICKS = pdMS_TO_TICKS(2000);
void AudioService::findStreamDevice() {
streamDevice = nullptr;
device_for_each_of_type(&AUDIO_STREAM_TYPE, &streamDevice, [](Device* device, void* context) -> bool {
if (device_is_ready(device)) {
*static_cast<Device**>(context) = device;
return false;
}
return true;
});
}
void AudioService::primeFromSettings() const {
if (streamDevice == nullptr) {
return;
}
auto settings = settings::audio::loadOrGetDefault();
audio_stream_set_enabled(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputEnabled);
audio_stream_set_enabled(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputEnabled);
audio_stream_set_mute(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputMuted);
audio_stream_set_mute(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputMuted);
audio_stream_set_volume(streamDevice, AUDIO_CODEC_DIR_INPUT, settings.inputVolume);
audio_stream_set_volume(streamDevice, AUDIO_CODEC_DIR_OUTPUT, settings.outputVolume);
}
void AudioService::schedulePersist() {
auto lock = mutex.asScopedLock();
lock.lock();
persistPending = true;
}
void AudioService::persistIfPending() {
bool shouldPersist;
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
shouldPersist = persistPending;
persistPending = false;
device = streamDevice;
}
if (!shouldPersist || device == nullptr) {
return;
}
// audio_stream_get_*() leave their out-param untouched (not zeroed) when the
// direction has no codec bound, returning ERROR_NOT_SUPPORTED instead -- starting
// from a freshly default-constructed (uninitialized) AudioSettings and ignoring
// those return values would persist garbage stack memory for the unsupported side.
// Start from the existing saved settings so an unsupported direction's fields are
// left at their last known-good value instead.
settings::audio::AudioSettings settings = settings::audio::loadOrGetDefault();
if (audio_stream_get_enabled(device, AUDIO_CODEC_DIR_INPUT, &settings.inputEnabled) == ERROR_NONE
&& audio_stream_get_mute(device, AUDIO_CODEC_DIR_INPUT, &settings.inputMuted) == ERROR_NONE) {
audio_stream_get_volume(device, AUDIO_CODEC_DIR_INPUT, &settings.inputVolume);
}
if (audio_stream_get_enabled(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputEnabled) == ERROR_NONE
&& audio_stream_get_mute(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputMuted) == ERROR_NONE) {
audio_stream_get_volume(device, AUDIO_CODEC_DIR_OUTPUT, &settings.outputVolume);
}
settings::audio::save(settings);
}
void AudioService::onDriverChangedCallback(Device* device, enum AudioCodecDirection direction, enum AudioStreamChange change, void* userData) {
static_cast<AudioService*>(userData)->onDriverChanged(direction, change);
}
void AudioService::onDriverChanged(enum AudioCodecDirection direction, enum AudioStreamChange change) {
bool isInput = (direction == AUDIO_CODEC_DIR_INPUT);
AudioEvent event;
switch (change) {
case AUDIO_STREAM_CHANGE_VOLUME:
event = isInput ? AudioEvent::InputVolumeChanged : AudioEvent::OutputVolumeChanged;
break;
case AUDIO_STREAM_CHANGE_MUTE:
event = isInput ? AudioEvent::InputMuteChanged : AudioEvent::OutputMuteChanged;
break;
case AUDIO_STREAM_CHANGE_ENABLED:
default:
event = isInput ? AudioEvent::InputEnabledChanged : AudioEvent::OutputEnabledChanged;
break;
}
// Called synchronously from the set*() caller's call stack, but AFTER it has already
// released mutex (see setInputEnabled() etc.) -- pubsub->publish() below re-enters this
// service (e.g. a subscriber's refresh() calling getOutputVolume()) on the same task,
// which would deadlock on the non-recursive Mutex type if we were still holding it here.
// Set the flag directly rather than via schedulePersist() to avoid taking mutex at all
// in this already-sensitive path.
persistPending = true;
pubsub->publish(event);
}
bool AudioService::onStart(ServiceContext& serviceContext) {
findStreamDevice();
if (streamDevice == nullptr) {
LOG_W(TAG, "No audio stream device found; service is unavailable");
return true;
}
primeFromSettings();
audio_stream_set_change_callback(streamDevice, &onDriverChangedCallback, this);
persistTimer = std::make_unique<Timer>(Timer::Type::Periodic, PERSIST_INTERVAL_TICKS, [this] {
persistIfPending();
});
persistTimer->start();
return true;
}
void AudioService::onStop(ServiceContext& serviceContext) {
if (persistTimer) {
persistTimer->stop();
persistTimer.reset();
}
persistIfPending();
{
auto lock = mutex.asScopedLock();
lock.lock();
if (streamDevice != nullptr) {
audio_stream_set_change_callback(streamDevice, nullptr, nullptr);
}
streamDevice = nullptr;
}
}
bool AudioService::isAvailable() const {
auto lock = mutex.asScopedLock();
lock.lock();
return streamDevice != nullptr;
}
bool AudioService::isInputAvailable() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool supported = false;
if (streamDevice != nullptr) {
audio_stream_is_supported(streamDevice, AUDIO_CODEC_DIR_INPUT, &supported);
}
return supported;
}
bool AudioService::isOutputAvailable() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool supported = false;
if (streamDevice != nullptr) {
audio_stream_is_supported(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &supported);
}
return supported;
}
bool AudioService::isInputEnabled() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool enabled = false;
if (streamDevice != nullptr) {
audio_stream_get_enabled(streamDevice, AUDIO_CODEC_DIR_INPUT, &enabled);
}
return enabled;
}
void AudioService::setInputEnabled(bool enabled) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// Must not hold mutex here: this can synchronously trigger onDriverChanged() ->
// pubsub->publish(), which re-enters this service (e.g. a subscriber's refresh()
// calling isInputEnabled()) on the SAME task/call-stack. mutex is a non-recursive
// FreeRTOS mutex, so holding it across this call deadlocks that re-entrant call.
audio_stream_set_enabled(device, AUDIO_CODEC_DIR_INPUT, enabled);
}
bool AudioService::isOutputEnabled() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool enabled = false;
if (streamDevice != nullptr) {
audio_stream_get_enabled(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &enabled);
}
return enabled;
}
void AudioService::setOutputEnabled(bool enabled) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// See setInputEnabled() for why mutex must be released before this call.
audio_stream_set_enabled(device, AUDIO_CODEC_DIR_OUTPUT, enabled);
}
float AudioService::getInputVolume() const {
auto lock = mutex.asScopedLock();
lock.lock();
float volume = 0.0f;
if (streamDevice != nullptr) {
audio_stream_get_volume(streamDevice, AUDIO_CODEC_DIR_INPUT, &volume);
}
return volume;
}
void AudioService::setInputVolume(float percent) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// See setInputEnabled() for why mutex must be released before this call.
audio_stream_set_volume(device, AUDIO_CODEC_DIR_INPUT, percent);
}
float AudioService::getOutputVolume() const {
auto lock = mutex.asScopedLock();
lock.lock();
float volume = 0.0f;
if (streamDevice != nullptr) {
audio_stream_get_volume(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &volume);
}
return volume;
}
void AudioService::setOutputVolume(float percent) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// See setInputEnabled() for why mutex must be released before this call.
audio_stream_set_volume(device, AUDIO_CODEC_DIR_OUTPUT, percent);
}
bool AudioService::isInputMuted() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool muted = false;
if (streamDevice != nullptr) {
audio_stream_get_mute(streamDevice, AUDIO_CODEC_DIR_INPUT, &muted);
}
return muted;
}
void AudioService::setInputMuted(bool muted) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// See setInputEnabled() for why mutex must be released before this call.
audio_stream_set_mute(device, AUDIO_CODEC_DIR_INPUT, muted);
}
bool AudioService::isOutputMuted() const {
auto lock = mutex.asScopedLock();
lock.lock();
bool muted = false;
if (streamDevice != nullptr) {
audio_stream_get_mute(streamDevice, AUDIO_CODEC_DIR_OUTPUT, &muted);
}
return muted;
}
void AudioService::setOutputMuted(bool muted) {
Device* device;
{
auto lock = mutex.asScopedLock();
lock.lock();
device = streamDevice;
}
if (device == nullptr) {
return;
}
// See setInputEnabled() for why mutex must be released before this call.
audio_stream_set_mute(device, AUDIO_CODEC_DIR_OUTPUT, muted);
}
// Precondition: AudioService must already be registered and started. Audio.cpp's wrapper
// functions do NOT use this (they must tolerate the service never having been registered
// on devices without audio hardware) - this is for callers that require the service to exist.
std::shared_ptr<AudioService> findAudioService() {
auto service = findServiceById(manifest.id);
assert(service != nullptr);
return std::static_pointer_cast<AudioService>(service);
}
extern const ServiceManifest manifest = {
.id = "Audio",
.createService = create<AudioService>
};
} // namespace tt::service::audio
+113
View File
@@ -0,0 +1,113 @@
#include <Tactility/settings/AudioSettings.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <string>
namespace tt::settings::audio {
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/audio.properties";
}
constexpr auto* SETTINGS_KEY_INPUT_ENABLED = "inputEnabled";
constexpr auto* SETTINGS_KEY_OUTPUT_ENABLED = "outputEnabled";
constexpr auto* SETTINGS_KEY_INPUT_MUTED = "inputMuted";
constexpr auto* SETTINGS_KEY_OUTPUT_MUTED = "outputMuted";
constexpr auto* SETTINGS_KEY_INPUT_VOLUME = "inputVolume";
constexpr auto* SETTINGS_KEY_OUTPUT_VOLUME = "outputVolume";
static bool toBool(const std::map<std::string, std::string>& map, const char* key, bool defaultValue) {
auto entry = map.find(key);
if (entry == map.end()) {
return defaultValue;
}
return (entry->second == "1" || entry->second == "true" || entry->second == "True");
}
static float toFloat(const std::map<std::string, std::string>& map, const char* key, float defaultValue) {
auto entry = map.find(key);
if (entry == map.end()) {
return defaultValue;
}
// Volume is documented/consumed as a 0..100 percentage (audio_stream_set_volume); clamp
// here so a hand-edited or corrupted properties file can't push out-of-range values
// through to the codec.
float value = std::strtof(entry->second.c_str(), nullptr);
return std::clamp(value, 0.0f, 100.0f);
}
static std::string toString(bool value) {
return value ? "1" : "0";
}
static std::string toString(float value) {
// std::to_string always emits 6 decimals (e.g. "20.000000"); volume is a 0..100
// percentage where whole-number precision is all that's meaningful here.
char buffer[16];
snprintf(buffer, sizeof(buffer), "%.1f", (double) value);
return std::string(buffer);
}
bool load(AudioSettings& settings) {
auto settings_path = getSettingsFilePath();
if (!file::isFile(settings_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
settings.inputEnabled = toBool(map, SETTINGS_KEY_INPUT_ENABLED, true);
settings.outputEnabled = toBool(map, SETTINGS_KEY_OUTPUT_ENABLED, true);
settings.inputMuted = toBool(map, SETTINGS_KEY_INPUT_MUTED, false);
settings.outputMuted = toBool(map, SETTINGS_KEY_OUTPUT_MUTED, false);
settings.inputVolume = toFloat(map, SETTINGS_KEY_INPUT_VOLUME, 90.0f);
settings.outputVolume = toFloat(map, SETTINGS_KEY_OUTPUT_VOLUME, 50.0f);
return true;
}
AudioSettings getDefault() {
return AudioSettings {
.inputEnabled = true,
.outputEnabled = true,
.inputMuted = false,
.outputMuted = false,
.inputVolume = 90.0f,
.outputVolume = 50.0f
};
}
AudioSettings loadOrGetDefault() {
AudioSettings settings;
if (!load(settings)) {
settings = getDefault();
}
return settings;
}
bool save(const AudioSettings& settings) {
std::map<std::string, std::string> map;
map[SETTINGS_KEY_INPUT_ENABLED] = toString(settings.inputEnabled);
map[SETTINGS_KEY_OUTPUT_ENABLED] = toString(settings.outputEnabled);
map[SETTINGS_KEY_INPUT_MUTED] = toString(settings.inputMuted);
map[SETTINGS_KEY_OUTPUT_MUTED] = toString(settings.outputMuted);
map[SETTINGS_KEY_INPUT_VOLUME] = toString(settings.inputVolume);
map[SETTINGS_KEY_OUTPUT_VOLUME] = toString(settings.outputVolume);
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
return false;
}
return file::savePropertiesFile(settings_path, map);
}
} // namespace tt::settings::audio