Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Apps/Breakout/main/Source/Breakout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
60 changes: 46 additions & 14 deletions Apps/MediaKeys/main/Source/MediaKeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,19 +211,49 @@ 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;
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
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.
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions Apps/MediaKeys/main/Source/MediaKeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <TactilityCpp/App.h>
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tt_app.h>
Expand All @@ -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<bool> _isEnabled {false};
std::atomic<bool> _radioEnabling{false}; // true while waiting for radio to come ON
std::atomic<bool> _radioWasOff {false}; // true if MediaKeys turned the radio on (so we turn it off)
std::atomic<bool> _isEnabled {false};
std::atomic<bool> _radioEnabling {false}; // true while waiting for radio to come ON
std::atomic<bool> _radioWasOff {false}; // true if we turned the radio on (restore on exit)
std::atomic<bool> _deviceWasStarted{false}; // true if we called device_start (restore on exit)

// Static event callbacks
static void onSwitchToggled(lv_event_t* e);
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion Apps/TamaTac/main/Source/TamaTac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 10 additions & 8 deletions Libraries/SfxEngine/Include/SfxEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include <cstdint>
#include <tactility/device.h>
#include <tactility/drivers/audio_stream.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 4 additions & 5 deletions Libraries/SfxEngine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading