diff --git a/Apps/Breakout/main/Source/Breakout.cpp b/Apps/Breakout/main/Source/Breakout.cpp index 7e12b83..4c9553a 100644 --- a/Apps/Breakout/main/Source/Breakout.cpp +++ b/Apps/Breakout/main/Source/Breakout.cpp @@ -124,7 +124,6 @@ void Breakout::onShow(AppHandle appHandle, lv_obj_t* parent) { if (!sfxEngine) { sfxEngine = new SfxEngine(); sfxEngine->start(); - sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Quiet); sfxEngine->setEnabled(soundEnabled); } diff --git a/Apps/MediaKeys/main/Source/MediaKeys.cpp b/Apps/MediaKeys/main/Source/MediaKeys.cpp index 4e5d8ab..3ae6b1a 100644 --- a/Apps/MediaKeys/main/Source/MediaKeys.cpp +++ b/Apps/MediaKeys/main/Source/MediaKeys.cpp @@ -211,12 +211,29 @@ void MediaKeys::startHid() { if (tt_lvgl_hardware_keyboard_is_available()) enterKeyMode(); } +void MediaKeys::teardownBt() { + // Remove callback FIRST - stops any in-flight BT events from firing against + // our (possibly already freed) UI widget pointers after this returns. + if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback); + // Do NOT call bluetooth_hid_device_stop here: it calls ble_gatts_reset() / + // ble_gatts_start() which corrupts NimBLE heap while the host task is still + // running. HID device is a persistent kernel device; hid_device_start() cleans + // up stale context on next use. Explicit stop is handled by handleSwitchToggle. + // Restore the radio/device to the state we found them in. + if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false); + if (_btDevice && _deviceWasStarted) device_stop(_btDevice); + _btDevice = nullptr; + _hidDevice = nullptr; + _radioWasOff = false; + _deviceWasStarted = false; +} + void MediaKeys::handleSwitchToggle(bool enabled) { LOG_I(TAG, "Switch: %s", enabled ? "ON" : "OFF"); _isEnabled = enabled; if (enabled) { - _btDevice = bluetooth_find_first_ready_device(); + _btDevice = device_find_first_by_type(&BLUETOOTH_TYPE); if (!_btDevice) { LOG_E(TAG, "No Bluetooth device found"); _isEnabled = false; @@ -224,6 +241,19 @@ void MediaKeys::handleSwitchToggle(bool enabled) { return; } + // Device may not be started yet (BT disabled in DTS by default to save memory). + if (!device_is_ready(_btDevice)) { + LOG_I(TAG, "BT device not started, starting now"); + if (device_start(_btDevice) != ERROR_NONE) { + LOG_E(TAG, "Failed to start BT device"); + _btDevice = nullptr; + _isEnabled = false; + if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED); + return; + } + _deviceWasStarted = true; + } + bluetooth_set_device_name(_btDevice, "Tactility Media Keys"); // Register callback before enabling radio so we don't miss the state-change event. @@ -247,12 +277,10 @@ void MediaKeys::handleSwitchToggle(bool enabled) { } else { _radioEnabling = false; if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode(); + // Explicit user toggle-off: stop HID cleanly (safe here since we're on the + // LVGL task and the user intentionally disabled, so no race with app teardown). if (_hidDevice) bluetooth_hid_device_stop(_hidDevice); - if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback); - if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false); - _radioWasOff = false; - _btDevice = nullptr; - _hidDevice = nullptr; + teardownBt(); if (_mainWrapper) lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN); } } @@ -321,19 +349,23 @@ void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) { } lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN); + + // Auto-enable if BT is already on (turned on via QuickPanel/Settings before opening app). + struct Device* btDev = device_find_first_by_type(&BLUETOOTH_TYPE); + if (btDev && device_is_ready(btDev)) { + enum BtRadioState radioState; + if (bluetooth_get_radio_state(btDev, &radioState) == ERROR_NONE && radioState == BT_RADIO_STATE_ON) { + lv_obj_add_state(_switchWidget, LV_STATE_CHECKED); + handleSwitchToggle(true); + } + } } void MediaKeys::onHide(AppHandle /*appHandle*/) { - if (_hidDevice) bluetooth_hid_device_stop(_hidDevice); - if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback); - if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false); - _btDevice = nullptr; - _hidDevice = nullptr; - _isEnabled = false; _radioEnabling = false; - _radioWasOff = false; - + _isEnabled = false; if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode(); + teardownBt(); if (_keyHighlightTimer) { lv_timer_delete(_keyHighlightTimer); _keyHighlightTimer = nullptr; diff --git a/Apps/MediaKeys/main/Source/MediaKeys.h b/Apps/MediaKeys/main/Source/MediaKeys.h index 825a061..eedc5be 100644 --- a/Apps/MediaKeys/main/Source/MediaKeys.h +++ b/Apps/MediaKeys/main/Source/MediaKeys.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -23,9 +24,10 @@ class MediaKeys final : public App { struct Device* _hidDevice = nullptr; // State - accessed from both LVGL thread and BT callback thread - std::atomic _isEnabled {false}; - std::atomic _radioEnabling{false}; // true while waiting for radio to come ON - std::atomic _radioWasOff {false}; // true if MediaKeys turned the radio on (so we turn it off) + std::atomic _isEnabled {false}; + std::atomic _radioEnabling {false}; // true while waiting for radio to come ON + std::atomic _radioWasOff {false}; // true if we turned the radio on (restore on exit) + std::atomic _deviceWasStarted{false}; // true if we called device_start (restore on exit) // Static event callbacks static void onSwitchToggled(lv_event_t* e); @@ -36,6 +38,7 @@ class MediaKeys final : public App { static void sendKeyTask(void* param); // Instance methods called by static callbacks + void teardownBt(); // remove callback + stop HID + restore radio/device state void handleSwitchToggle(bool enabled); void handleButtonPress(uint32_t buttonId); void startHid(); // called once radio is confirmed ON diff --git a/Apps/TamaTac/main/Source/TamaTac.cpp b/Apps/TamaTac/main/Source/TamaTac.cpp index ac22b66..b60a703 100644 --- a/Apps/TamaTac/main/Source/TamaTac.cpp +++ b/Apps/TamaTac/main/Source/TamaTac.cpp @@ -57,7 +57,6 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) { if (sfxEngine == nullptr) { sfxEngine = new SfxEngine(); sfxEngine->start(); - sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Normal); // Load settings bool soundEnabled; diff --git a/Libraries/SfxEngine/Include/SfxEngine.h b/Libraries/SfxEngine/Include/SfxEngine.h index 4e96e52..8002f3a 100644 --- a/Libraries/SfxEngine/Include/SfxEngine.h +++ b/Libraries/SfxEngine/Include/SfxEngine.h @@ -26,6 +26,7 @@ #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" @@ -161,12 +162,6 @@ class SfxEngine { // Settings void setEnabled(bool enabled) { enabled_ = enabled; } bool isEnabled() const { return enabled_; } - void setVolume(float vol) { masterVolume_ = (vol < 0) ? 0 : (vol > 1.0f) ? 1.0f : vol; } - float getVolume() const { return masterVolume_; } - - // Volume presets (consistent with SoundEngine naming) - enum class VolumePreset { Quiet, Normal, Loud }; - void applyVolumePreset(VolumePreset preset); // Polyphonic gate (consistent with SoundEngine) void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; } @@ -277,14 +272,21 @@ class SfxEngine { // State //-------------------------------------------------------------------------- - Device* i2sDevice_ = nullptr; + Device* audioStreamDevice_ = nullptr; + AudioStreamHandle audioStreamHandle_ = nullptr; TaskHandle_t task_ = nullptr; SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits QueueHandle_t msgQueue_ = nullptr; volatile bool running_ = false; volatile bool enabled_ = true; - volatile float masterVolume_ = 0.5f; + + // Cached system output volume (0..1), refreshed periodically from the shared + // audio_stream device so the preset acts as a relative multiplier on top of it + // rather than an absolute level (a "Quiet" preset should sound quiet relative + // to whatever the user has the system volume set to, not in absolute terms). + float systemVolumeMix_ = 1.0f; + int systemVolumePollCounter_ = 0; // Polyphonic gate volatile bool polyphonicGateEnabled_ = true; diff --git a/Libraries/SfxEngine/README.md b/Libraries/SfxEngine/README.md index 7d7ecc3..ab716ca 100644 --- a/Libraries/SfxEngine/README.md +++ b/Libraries/SfxEngine/README.md @@ -35,11 +35,8 @@ if (!engine->start()) { ESP_LOGE(TAG, "Failed to start SfxEngine"); return; } -engine->applyVolumePreset(SfxEngine::VolumePreset::Normal); - engine->play(SfxId::Coin); // Predefined SFX engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms -engine->setVolume(0.7f); // Volume control engine->stop(); delete engine; @@ -87,9 +84,11 @@ idf_component_register( - `void stopVoice(voice)` - Stop specific voice ### Settings -- `void setVolume(float)` - Master volume (0.0-1.0, exponential curve) - `void setEnabled(bool)` - Mute/unmute -- `void applyVolumePreset(VolumePreset)` - Apply Quiet/Normal/Loud preset (configures volume, gate, normalization) + +Loudness is controlled by the system output volume (set via the audio_stream device / Settings UI), +not by SfxEngine itself -- a fixed app-side gain on top of hardware attenuation gets swamped at low +system volumes, so there's no separate volume control here. ### Mixing (consistent with SoundEngine) - `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on) diff --git a/Libraries/SfxEngine/Source/SfxEngine.cpp b/Libraries/SfxEngine/Source/SfxEngine.cpp index a6fd942..862bfec 100644 --- a/Libraries/SfxEngine/Source/SfxEngine.cpp +++ b/Libraries/SfxEngine/Source/SfxEngine.cpp @@ -9,7 +9,7 @@ #include "SfxEngine.h" #include "SfxDefinitions.h" -#include +#include #include #include #include "esp_log.h" @@ -320,15 +320,17 @@ void SfxEngine::fillStereoBuffer(int16_t* buf, int samples) { // Apply polyphonic soft gate (proportional reduction when clipping threatened) mix = applyPolyphonicGate(mix, activeVoices); - // Apply master volume (exponential curve for perceptual linearity) - float volCurve = masterVolume_ * masterVolume_; - mix *= volCurve; - // Apply auto-normalization (consistent volume across different SFX) mix = applyAutoNormalization(mix); // Brick-wall limiter (final safety net before soft clip) mix = applyBrickWallLimiter(mix); + + // The shared system output volume (esp_codec_dev hardware attenuation, set via + // the Settings UI / audio_stream_set_volume) is the sole loudness control here -- + // a fixed app-side gain multiplier stacked on top of it just gets swamped at low + // system-volume levels, making any such control feel like it does nothing. + mix *= systemVolumeMix_; } // Cubic soft clip @@ -460,14 +462,28 @@ void SfxEngine::audioTaskFunc(void* param) { } } + // Periodically refresh the cached system output volume/enabled state (cheap pass- + // through to the shared kernel device; polled rather than read per-sample since it + // changes rarely and audio_stream_get_volume may take a lock). + if (self->systemVolumePollCounter_-- <= 0) { + self->systemVolumePollCounter_ = 32; // ~0.5s at 256 samples / 16kHz + + float systemVolumePercent = 100.0f; + bool systemOutputEnabled = true; + audio_stream_get_volume(self->audioStreamDevice_, AUDIO_CODEC_DIR_OUTPUT, &systemVolumePercent); + audio_stream_get_enabled(self->audioStreamDevice_, AUDIO_CODEC_DIR_OUTPUT, &systemOutputEnabled); + + self->systemVolumeMix_ = systemOutputEnabled ? (systemVolumePercent / 100.0f) : 0.0f; + } + // Fill audio buffer (member buffer to avoid stack pressure) self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES); - // Write to I2S - error_t error = i2s_controller_write(self->i2sDevice_, self->audioBuffer_, - sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100)); + // Write to the audio stream (resampled to the codec's native rate transparently) + error_t error = audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, + sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100)); if (error != ERROR_NONE) { - ESP_LOGE(TAG, "I2S write error"); + ESP_LOGE(TAG, "Audio stream write error"); self->running_ = false; break; } @@ -475,7 +491,7 @@ void SfxEngine::audioTaskFunc(void* param) { // Flush silence memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_)); - i2s_controller_write(self->i2sDevice_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50)); + audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50)); ESP_LOGI(TAG, "Audio task exiting"); @@ -494,33 +510,31 @@ void SfxEngine::audioTaskFunc(void* param) { bool SfxEngine::start() { if (running_) return true; - // Find I2S device - i2sDevice_ = nullptr; - device_for_each_of_type(&I2S_CONTROLLER_TYPE, &i2sDevice_, [](Device* device, void* context) { + // Find audio stream device + audioStreamDevice_ = nullptr; + device_for_each_of_type(&AUDIO_STREAM_TYPE, &audioStreamDevice_, [](Device* device, void* context) { if (!device_is_ready(device)) return true; Device** devicePtr = static_cast(context); *devicePtr = device; return false; }); - if (i2sDevice_ == nullptr) { - ESP_LOGW(TAG, "No I2S device found"); + if (audioStreamDevice_ == nullptr) { + ESP_LOGW(TAG, "No audio stream device found"); return false; } - // Configure I2S - I2sConfig config = { - .communication_format = I2S_FORMAT_STAND_I2S, + // Open output stream (the kernel resamples to the codec's native rate transparently) + AudioStreamConfig config = { .sample_rate = SAMPLE_RATE, .bits_per_sample = 16, - .channel_left = 0, - .channel_right = 0 + .channels = 2 }; - error_t error = i2s_controller_set_config(i2sDevice_, &config); + error_t error = audio_stream_open_output(audioStreamDevice_, &config, &audioStreamHandle_); if (error != ERROR_NONE) { - ESP_LOGE(TAG, "Failed to configure I2S: %s", error_to_string(error)); - i2sDevice_ = nullptr; + ESP_LOGE(TAG, "Failed to open audio output stream: %s", error_to_string(error)); + audioStreamDevice_ = nullptr; return false; } @@ -528,12 +542,14 @@ bool SfxEngine::start() { msgQueue_ = xQueueCreate(8, sizeof(QueueMsg)); if (msgQueue_ == nullptr) { ESP_LOGE(TAG, "Failed to create message queue"); - i2s_controller_reset(i2sDevice_); - i2sDevice_ = nullptr; + audio_stream_close(audioStreamHandle_); + audioStreamHandle_ = nullptr; + audioStreamDevice_ = nullptr; return false; } // Start audio task + systemVolumePollCounter_ = 0; // poll the system volume immediately on the first iteration running_ = true; BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_); if (result != pdPASS) { @@ -541,8 +557,9 @@ bool SfxEngine::start() { running_ = false; vQueueDelete(msgQueue_); msgQueue_ = nullptr; - i2s_controller_reset(i2sDevice_); - i2sDevice_ = nullptr; + audio_stream_close(audioStreamHandle_); + audioStreamHandle_ = nullptr; + audioStreamDevice_ = nullptr; return false; } @@ -575,43 +592,15 @@ void SfxEngine::stop() { msgQueue_ = nullptr; } - if (i2sDevice_ != nullptr) { - i2s_controller_reset(i2sDevice_); - i2sDevice_ = nullptr; + if (audioStreamHandle_ != nullptr) { + audio_stream_close(audioStreamHandle_); + audioStreamHandle_ = nullptr; } + audioStreamDevice_ = nullptr; ESP_LOGI(TAG, "SfxEngine stopped"); } -void SfxEngine::applyVolumePreset(VolumePreset preset) { - switch (preset) { - case VolumePreset::Quiet: - masterVolume_ = 0.3f; - autoNormalize_ = true; - targetRms_ = 0.25f; - polyphonicGateEnabled_ = true; - softGateThreshold_ = 0.90f; - ESP_LOGI(TAG, "Applied Quiet preset"); - break; - case VolumePreset::Normal: - masterVolume_ = 0.5f; - autoNormalize_ = true; - targetRms_ = 0.35f; - polyphonicGateEnabled_ = true; - softGateThreshold_ = 0.95f; - ESP_LOGI(TAG, "Applied Normal preset"); - break; - case VolumePreset::Loud: - masterVolume_ = 0.75f; - autoNormalize_ = true; - targetRms_ = 0.45f; - polyphonicGateEnabled_ = true; - softGateThreshold_ = 0.98f; - ESP_LOGI(TAG, "Applied Loud preset"); - break; - } -} - void SfxEngine::play(SfxId sound) { if (!running_ || msgQueue_ == nullptr) return;