diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 9121cb5a..f5ce2957 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -5,6 +5,7 @@ */ #include "audio_fsm.hpp" +#include #include #include @@ -29,6 +30,7 @@ #include "system_events.hpp" #include "track.hpp" #include "track_queue.hpp" +#include "wm8523.hpp" namespace audio { @@ -39,6 +41,7 @@ std::shared_ptr AudioState::sServices; std::shared_ptr AudioState::sFileSource; std::unique_ptr AudioState::sDecoder; std::shared_ptr AudioState::sSampleConverter; +std::shared_ptr AudioState::sI2SOutput; std::shared_ptr AudioState::sOutput; std::optional AudioState::sCurrentTrack; @@ -65,6 +68,13 @@ void AudioState::react(const system_fsm::HasPhonesChanged& ev) { } } +void AudioState::react(const ChangeMaxVolume& ev) { + ESP_LOGI(kTag, "new max volume %u db", + (ev.new_max - drivers::wm8523::kLineLevelReferenceVolume) / 4); + sI2SOutput->SetMaxVolume(ev.new_max); + sServices->nvs().AmpMaxVolume(ev.new_max); +} + namespace states { void Uninitialised::react(const system_fsm::BootComplete& ev) { @@ -78,8 +88,14 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) { } sFileSource.reset(new FatfsAudioInput(sServices->tag_parser())); - sOutput.reset(new I2SAudioOutput(sServices->gpios(), - std::unique_ptr{*dac})); + sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), + std::unique_ptr{*dac})); + + auto& nvs = sServices->nvs(); + sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume().get()); + sI2SOutput->SetVolumeDb(nvs.AmpCurrentVolume().get()); + + sOutput = sI2SOutput; // sOutput.reset(new BluetoothAudioOutput(bluetooth)); sSampleConverter.reset(new SampleConverter()); diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 927f6541..68b03145 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -5,6 +5,7 @@ */ #include "i2s_audio_output.hpp" +#include #include #include @@ -48,9 +49,8 @@ I2SAudioOutput::I2SAudioOutput(drivers::IGpios& expander, dac_(std::move(dac)), current_config_(), left_difference_(0), - current_volume_(kDefaultVolume), - max_volume_(kLineLevelVolume) { - SetVolume(GetVolume()); + current_volume_(0), + max_volume_(0) { dac_->SetSource(stream()); } @@ -72,6 +72,18 @@ auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void { SetVolume(GetVolume()); } +auto I2SAudioOutput::SetMaxVolume(uint16_t max) -> void { + max_volume_ = std::clamp(max, drivers::wm8523::kAbsoluteMinVolume, + drivers::wm8523::kAbsoluteMaxVolume); + SetVolume(GetVolume()); +} + +auto I2SAudioOutput::SetVolumeDb(uint16_t vol) -> void { + current_volume_ = + std::clamp(vol, drivers::wm8523::kAbsoluteMinVolume, max_volume_); + SetVolume(GetVolume()); +} + auto I2SAudioOutput::SetVolume(uint_fast8_t percent) -> void { percent = std::min(percent, 100); float new_value = static_cast(max_volume_) / 100 * percent; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 8ee8b057..7433d159 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -36,6 +37,9 @@ struct PlayFile : tinyfsm::Event { }; struct VolumeChanged : tinyfsm::Event {}; +struct ChangeMaxVolume : tinyfsm::Event { + uint16_t new_max; +}; struct TogglePlayPause : tinyfsm::Event {}; diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 1376feae..46d3d338 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -44,6 +44,7 @@ class AudioState : public tinyfsm::Fsm { void react(const system_fsm::KeyUpChanged&); void react(const system_fsm::KeyDownChanged&); void react(const system_fsm::HasPhonesChanged&); + void react(const ChangeMaxVolume&); virtual void react(const system_fsm::BootComplete&) {} @@ -63,6 +64,7 @@ class AudioState : public tinyfsm::Fsm { static std::shared_ptr sFileSource; static std::unique_ptr sDecoder; static std::shared_ptr sSampleConverter; + static std::shared_ptr sI2SOutput; static std::shared_ptr sOutput; static std::optional sCurrentTrack; diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index fa09deef..17f6b71a 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -25,6 +26,9 @@ class I2SAudioOutput : public IAudioOutput { auto SetInUse(bool) -> void override; + auto SetMaxVolume(uint16_t) -> void; + auto SetVolumeDb(uint16_t) -> void; + auto SetVolumeImbalance(int_fast8_t balance) -> void override; auto SetVolume(uint_fast8_t percent) -> void override; auto GetVolume() -> uint_fast8_t override; diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index bc88f88d..d7b3dfdd 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -37,6 +37,12 @@ class NvsStorage { auto ScreenBrightness() -> std::future; auto ScreenBrightness(uint_fast8_t) -> std::future; + auto AmpMaxVolume() -> std::future; + auto AmpMaxVolume(uint16_t) -> std::future; + + auto AmpCurrentVolume() -> std::future; + auto AmpCurrentVolume(uint16_t) -> std::future; + explicit NvsStorage(std::unique_ptr, nvs_handle_t); ~NvsStorage(); diff --git a/src/drivers/include/wm8523.hpp b/src/drivers/include/wm8523.hpp index 6dc2e56e..b13cf34b 100644 --- a/src/drivers/include/wm8523.hpp +++ b/src/drivers/include/wm8523.hpp @@ -5,12 +5,32 @@ */ #pragma once +#include #include #include namespace drivers { namespace wm8523 { +extern const uint16_t kAbsoluteMaxVolume; + +extern const uint16_t kAbsoluteMinVolume; + +extern const uint16_t kMaxVolumeBeforeClipping; + +extern const uint16_t kLineLevelReferenceVolume; + +extern const uint16_t kDefaultVolume; +extern const uint16_t kDefaultMaxVolume; + +constexpr auto VolumeToDb(uint16_t vol) -> int_fast8_t { + return (vol - kLineLevelReferenceVolume) / 4; +} + +constexpr auto DbToVolume(int_fast8_t db) -> uint16_t { + return (db * 4) + kLineLevelReferenceVolume; +} + enum class Register : uint8_t { kReset = 0, kRevision = 1, diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index c2832bf4..7bd1afe2 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -17,6 +17,7 @@ #include "nvs.h" #include "nvs_flash.h" #include "tasks.hpp" +#include "wm8523.hpp" namespace drivers { @@ -27,6 +28,8 @@ static constexpr char kKeyVersion[] = "ver"; static constexpr char kKeyBluetooth[] = "bt"; static constexpr char kKeyOutput[] = "out"; static constexpr char kKeyBrightness[] = "bright"; +static constexpr char kKeyAmpMaxVolume[] = "hp_vol_max"; +static constexpr char kKeyAmpCurrentVolume[] = "hp_vol"; auto NvsStorage::OpenSync() -> NvsStorage* { esp_err_t err = nvs_flash_init(); @@ -154,4 +157,34 @@ auto NvsStorage::ScreenBrightness(uint_fast8_t val) -> std::future { }); } +auto NvsStorage::AmpMaxVolume() -> std::future { + return writer_->Dispatch([&]() -> uint16_t { + uint16_t out = wm8523::kDefaultMaxVolume; + nvs_get_u16(handle_, kKeyAmpMaxVolume, &out); + return out; + }); +} + +auto NvsStorage::AmpMaxVolume(uint16_t val) -> std::future { + return writer_->Dispatch([&]() { + nvs_set_u16(handle_, kKeyAmpMaxVolume, val); + return nvs_commit(handle_) == ESP_OK; + }); +} + +auto NvsStorage::AmpCurrentVolume() -> std::future { + return writer_->Dispatch([&]() -> uint16_t { + uint16_t out = wm8523::kDefaultVolume; + nvs_get_u16(handle_, kKeyAmpCurrentVolume, &out); + return out; + }); +} + +auto NvsStorage::AmpCurrentVolume(uint16_t val) -> std::future { + return writer_->Dispatch([&]() { + nvs_set_u16(handle_, kKeyAmpCurrentVolume, val); + return nvs_commit(handle_) == ESP_OK; + }); +} + } // namespace drivers diff --git a/src/drivers/wm8523.cpp b/src/drivers/wm8523.cpp index e1dffd51..5f6c8053 100644 --- a/src/drivers/wm8523.cpp +++ b/src/drivers/wm8523.cpp @@ -15,6 +15,23 @@ namespace drivers { namespace wm8523 { +const uint16_t kAbsoluteMaxVolume = 0x1ff; +const uint16_t kAbsoluteMinVolume = 0b0; + +// This is 3dB below what the DAC considers to be '0dB', and 9.5dB above line +// level reference. +const uint16_t kMaxVolumeBeforeClipping = 0x184; + +// This is 12.5 dB below what the DAC considers to be '0dB'. +const uint16_t kLineLevelReferenceVolume = 0x15E; + +// Default to -24 dB, which I will claim is 'arbitrarily chosen to be safe but +// audible', but is in fact just a nice value for my headphones in particular. +const uint16_t kDefaultVolume = kLineLevelReferenceVolume - 96; + +// Default to +6dB == 2Vrms == 'CD Player' +const uint16_t kDefaultMaxVolume = kLineLevelReferenceVolume + 12; + static const uint8_t kAddress = 0b0011010; auto ReadRegister(Register reg) -> std::optional { diff --git a/src/ui/include/screen_settings.hpp b/src/ui/include/screen_settings.hpp index 0ec96d26..caa23fd4 100644 --- a/src/ui/include/screen_settings.hpp +++ b/src/ui/include/screen_settings.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -32,7 +33,20 @@ class Bluetooth : public MenuScreen { class Headphones : public MenuScreen { public: - Headphones(); + Headphones(drivers::NvsStorage& nvs); + + auto ChangeMaxVolume(uint8_t index) -> void; + auto ChangeCustomVolume(int8_t diff) -> void; + + private: + auto UpdateCustomVol(uint16_t) -> void; + + drivers::NvsStorage& nvs_; + lv_obj_t* custom_vol_container_; + lv_obj_t* custom_vol_label_; + + std::vector index_to_level_; + uint16_t custom_limit_; }; class Appearance : public MenuScreen { diff --git a/src/ui/screen_settings.cpp b/src/ui/screen_settings.cpp index a11a7de6..d8c867dc 100644 --- a/src/ui/screen_settings.cpp +++ b/src/ui/screen_settings.cpp @@ -5,8 +5,10 @@ */ #include "screen_settings.hpp" +#include #include +#include "audio_events.hpp" #include "core/lv_event.h" #include "core/lv_obj.h" #include "display.hpp" @@ -18,6 +20,7 @@ #include "extra/layouts/flex/lv_flex.h" #include "extra/widgets/list/lv_list.h" #include "extra/widgets/menu/lv_menu.h" +#include "extra/widgets/spinbox/lv_spinbox.h" #include "extra/widgets/spinner/lv_spinner.h" #include "hal/lv_hal_disp.h" #include "index.hpp" @@ -34,6 +37,7 @@ #include "widgets/lv_label.h" #include "widgets/lv_slider.h" #include "widgets/lv_switch.h" +#include "wm8523.hpp" namespace ui { namespace screens { @@ -120,19 +124,68 @@ Bluetooth::Bluetooth() : MenuScreen("Bluetooth") { "this one has a really long name")); } -Headphones::Headphones() : MenuScreen("Headphones") { +static void change_vol_limit_cb(lv_event_t* ev) { + int selected_index = lv_dropdown_get_selected(ev->target); + Headphones* instance = reinterpret_cast(ev->user_data); + instance->ChangeMaxVolume(selected_index); +} + +static void increase_vol_limit_cb(lv_event_t* ev) { + Headphones* instance = reinterpret_cast(ev->user_data); + instance->ChangeCustomVolume(2); +} + +static void decrease_vol_limit_cb(lv_event_t* ev) { + Headphones* instance = reinterpret_cast(ev->user_data); + instance->ChangeCustomVolume(-2); +} + +Headphones::Headphones(drivers::NvsStorage& nvs) + : MenuScreen("Headphones"), nvs_(nvs), custom_limit_(0) { + uint16_t reference = drivers::wm8523::kLineLevelReferenceVolume; + index_to_level_.push_back(reference - (10 * 4)); + index_to_level_.push_back(reference + (6 * 4)); + index_to_level_.push_back(reference + (9.5 * 4)); + lv_obj_t* vol_label = lv_label_create(content_); lv_label_set_text(vol_label, "Volume Limit"); lv_obj_t* vol_dropdown = lv_dropdown_create(content_); lv_dropdown_set_options(vol_dropdown, - "Line Level (-10 dBV)\nPro Level (+4 dBu)\nMax " - "before clipping\nUnlimited\nCustom"); + "Line Level (-10 dB)\nCD Level (+6 dB)\nMax " + "before clipping (+10dB)\nCustom"); lv_group_add_obj(group_, vol_dropdown); - lv_obj_t* warning_label = label_pair( - content_, "!!", "Changing volume limit is for advanced users."); - lv_label_set_long_mode(warning_label, LV_LABEL_LONG_WRAP); - lv_obj_set_flex_grow(warning_label, 1); + uint16_t level = nvs.AmpMaxVolume().get(); + for (int i = 0; i < index_to_level_.size() + 1; i++) { + if (i == index_to_level_.size() || index_to_level_[i] == level) { + lv_dropdown_set_selected(vol_dropdown, i); + break; + } + } + + lv_obj_add_event_cb(vol_dropdown, change_vol_limit_cb, LV_EVENT_VALUE_CHANGED, + this); + + custom_vol_container_ = settings_container(content_); + + lv_obj_t* decrease_btn = lv_btn_create(custom_vol_container_); + lv_obj_t* btn_label = lv_label_create(decrease_btn); + lv_label_set_text(btn_label, "-"); + lv_obj_add_event_cb(decrease_btn, decrease_vol_limit_cb, LV_EVENT_CLICKED, + this); + + custom_vol_label_ = lv_label_create(custom_vol_container_); + UpdateCustomVol(level); + + lv_obj_t* increase_btn = lv_btn_create(custom_vol_container_); + btn_label = lv_label_create(increase_btn); + lv_label_set_text(btn_label, "+"); + lv_obj_add_event_cb(increase_btn, increase_vol_limit_cb, LV_EVENT_CLICKED, + this); + + if (lv_dropdown_get_selected(vol_dropdown) != index_to_level_.size()) { + lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); + } lv_obj_t* balance_label = lv_label_create(content_); lv_label_set_text(balance_label, "Left/Right Balance"); @@ -147,6 +200,41 @@ Headphones::Headphones() : MenuScreen("Headphones") { lv_obj_set_size(current_balance_label, lv_pct(100), LV_SIZE_CONTENT); } +auto Headphones::ChangeMaxVolume(uint8_t index) -> void { + if (index >= index_to_level_.size()) { + lv_obj_clear_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); + return; + } + auto vol = index_to_level_[index]; + lv_obj_add_flag(custom_vol_container_, LV_OBJ_FLAG_HIDDEN); + UpdateCustomVol(vol); + events::Audio().Dispatch(audio::ChangeMaxVolume{.new_max = vol}); +} + +auto Headphones::ChangeCustomVolume(int8_t diff) -> void { + UpdateCustomVol(custom_limit_ + diff); +} + +auto Headphones::UpdateCustomVol(uint16_t level) -> void { + custom_limit_ = level; + int16_t db = (static_cast(level) - + drivers::wm8523::kLineLevelReferenceVolume) / + 4; + int16_t db_parts = (static_cast(level) - + drivers::wm8523::kLineLevelReferenceVolume) % + 4; + + std::ostringstream builder; + if (db >= 0) { + builder << "+"; + } + builder << db << "."; + builder << (db_parts * 100 / 4); + builder << " dBV"; + + lv_label_set_text(custom_vol_label_, builder.str().c_str()); +} + static void change_brightness_cb(lv_event_t* ev) { Appearance* instance = reinterpret_cast(ev->user_data); instance->ChangeBrightness(lv_slider_get_value(ev->target)); diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 0054db23..e874418b 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -171,7 +171,7 @@ void Browse::react(const internal::ShowSettingsPage& ev) { screen.reset(new screens::Bluetooth()); break; case internal::ShowSettingsPage::Page::kHeadphones: - screen.reset(new screens::Headphones()); + screen.reset(new screens::Headphones(sServices->nvs())); break; case internal::ShowSettingsPage::Page::kAppearance: screen.reset(new screens::Appearance(sServices->nvs(), *sDisplay));