Audio System + Drivers (#562)
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user