mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
- genericBuzzer I2S path (BUZZER_I2S): an audio task synthesises the notes with eased edges and keeps the DMA queue fed with silence between sounds; ES8311 setup; amp powered through the board only around sounds - ui-core/SoundControl.h (mode, volume, built-in sounds, melody model), SoundNotifier moved to the Core, chanctl::melody, schema Sound page - L2: notifications, alarm / timer ring, locator sounds, Settings > Sound, melody editor with the playing note lit, per-chat sound, mute icon - noteIndex() on every buzzer backend; scrolling popups past screen height Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
469 lines
17 KiB
C++
469 lines
17 KiB
C++
// Sound: the speaker (genericBuzzer through the L2's codec), notify(),
|
|
// Settings > Sound's own rows (the mode, the two melodies), the melody editor
|
|
// (SCR_MELODY) and a conversation's melody. The settings and the melody model
|
|
// are ui-core/SoundControl.h; which sound a message plays, SoundNotifier.h.
|
|
// Included by UITask.cpp (single translation unit).
|
|
|
|
namespace sndview {
|
|
|
|
enum : uint8_t { MS_BPM, MS_PITCH, MS_OCT, MS_DUR };
|
|
enum : uint8_t { MA_PLAY, MA_ADD, MA_DELETE };
|
|
|
|
static soundctl::Melody s_mel; // the melody being edited
|
|
static int s_slot = 0; // 0 = melody 1, 1 = melody 2
|
|
static int s_sel = 0; // selected note
|
|
static bool s_dirty = false;
|
|
static lv_obj_t* s_strip = nullptr; // the notes, side by side
|
|
static lv_obj_t* s_cells[soundctl::MAX_NOTES];
|
|
static lv_obj_t* s_pitch = nullptr;
|
|
static lv_obj_t* s_oct = nullptr;
|
|
static lv_obj_t* s_dur = nullptr;
|
|
static lv_obj_t* s_bpm = nullptr;
|
|
static lv_obj_t* s_title = nullptr; // "Melodies 3/32"
|
|
static lv_obj_t* s_play_lbl = nullptr;
|
|
static bool s_playing_shown = false;
|
|
static bool s_play_all = false; // Play (not a note's preview): follow it in the strip
|
|
static int s_hl = -1; // the note lit as playing
|
|
// The melody playing from here: the sim's and nRF52's players keep a pointer
|
|
// into it (only the I2S one copies), so it must outlive the call.
|
|
static char s_play_buf[220];
|
|
|
|
static const char* PITCHES[] = { "Rest", "C", "D", "E", "F", "G", "A", "B", "" };
|
|
static const char* OCTAVES[] = { "4", "5", "6", "" };
|
|
static const char* DURS[] = { "1/4", "1/8", "1/16", "1/32", "" };
|
|
static const char* SLOTS[] = { "Melody 1", "Melody 2", "" };
|
|
|
|
// Selected: dim accent with a border; sounding now (Play): full accent.
|
|
static void styleCell(int i, bool sel) {
|
|
if (i < 0 || i >= soundctl::MAX_NOTES || !s_cells[i]) return;
|
|
uint32_t bg = i == s_hl ? theme::ACCENT : sel ? theme::ACCENT_DIM : theme::SURFACE;
|
|
lv_obj_set_style_bg_color(s_cells[i], lv_color_hex(bg), 0);
|
|
lv_obj_set_style_border_width(s_cells[i], sel ? 2 : 0, 0);
|
|
}
|
|
|
|
// Moves the "sounding" light to note `idx` (-1: none).
|
|
static void lightNote(int idx) {
|
|
if (idx == s_hl) return;
|
|
int old = s_hl;
|
|
s_hl = idx;
|
|
styleCell(old, old == s_sel);
|
|
styleCell(s_hl, s_hl == s_sel);
|
|
if (s_hl >= 0 && s_hl < s_mel.len && s_cells[s_hl]) lv_obj_scroll_to_view(s_cells[s_hl], LV_ANIM_ON);
|
|
}
|
|
|
|
static void cellText(int i) {
|
|
if (!s_cells[i]) return;
|
|
char name[3];
|
|
soundctl::noteName(s_mel.notes[i], name);
|
|
lv_label_set_text(lv_obj_get_child(s_cells[i], 0), name);
|
|
lv_label_set_text(lv_obj_get_child(s_cells[i], 1), soundctl::durLabel(soundctl::noteDur(s_mel.notes[i])));
|
|
}
|
|
|
|
static void refreshCount() {
|
|
if (s_title) lv_label_set_text_fmt(s_title, "Melodies %d/%d", s_mel.len, soundctl::MAX_NOTES);
|
|
}
|
|
|
|
static void setMatrix(lv_obj_t* m, int sel, bool enabled) {
|
|
if (!m) return;
|
|
lv_buttonmatrix_clear_button_ctrl_all(m, LV_BUTTONMATRIX_CTRL_CHECKED);
|
|
if (sel >= 0) lv_buttonmatrix_set_button_ctrl(m, sel, LV_BUTTONMATRIX_CTRL_CHECKED);
|
|
if (enabled) lv_buttonmatrix_clear_button_ctrl_all(m, LV_BUTTONMATRIX_CTRL_DISABLED);
|
|
else lv_buttonmatrix_set_button_ctrl_all(m, LV_BUTTONMATRIX_CTRL_DISABLED);
|
|
}
|
|
|
|
// The pitch / octave / length controls show the selected note.
|
|
static void syncControls() {
|
|
bool any = s_sel < s_mel.len;
|
|
uint8_t b = any ? s_mel.notes[s_sel] : 0;
|
|
setMatrix(s_pitch, any ? soundctl::notePitch(b) : -1, any);
|
|
setMatrix(s_oct, any && soundctl::notePitch(b) ? soundctl::noteOctave(b) - soundctl::OCT_MIN : -1,
|
|
any && soundctl::notePitch(b)); // a rest has no octave
|
|
setMatrix(s_dur, any ? soundctl::noteDur(b) : -1, any);
|
|
refreshCount();
|
|
}
|
|
|
|
static void onMelNote(lv_event_t* e);
|
|
|
|
static void rebuildStrip() {
|
|
if (!s_strip) return;
|
|
lv_obj_clean(s_strip);
|
|
memset(s_cells, 0, sizeof(s_cells));
|
|
s_hl = -1;
|
|
if (!s_mel.len) {
|
|
lv_obj_t* t = label(s_strip, "Empty: tap + to add a note", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_obj_set_style_pad_top(t, 12, 0);
|
|
}
|
|
for (int i = 0; i < s_mel.len; i++) {
|
|
lv_obj_t* c = lv_button_create(s_strip);
|
|
lv_obj_set_size(c, 38, 44);
|
|
lv_obj_set_style_pad_all(c, 0, 0);
|
|
lv_obj_set_style_radius(c, 6, 0);
|
|
lv_obj_set_style_shadow_width(c, 0, 0);
|
|
lv_obj_set_style_border_color(c, lv_color_hex(theme::ACCENT), 0);
|
|
lv_obj_add_event_cb(c, onMelNote, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
|
lv_obj_align(label(c, "", THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_MID, 0, 5);
|
|
lv_obj_align(label(c, "", THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_BOTTOM_MID, 0, -4);
|
|
s_cells[i] = c;
|
|
cellText(i);
|
|
styleCell(i, i == s_sel);
|
|
}
|
|
lv_obj_update_layout(s_strip);
|
|
if (s_sel < s_mel.len) lv_obj_scroll_to_view(s_cells[s_sel], LV_ANIM_OFF);
|
|
}
|
|
|
|
// A short blip of the note, as it's picked or changed.
|
|
static void preview(UITask* ui, uint8_t b) {
|
|
s_play_all = false;
|
|
lightNote(-1);
|
|
ui->stopMelody(); // before the buffer changes under the player
|
|
soundctl::notePreview(b, s_play_buf, sizeof(s_play_buf));
|
|
if (s_play_buf[0]) ui->playMelody(s_play_buf);
|
|
}
|
|
|
|
static void onSoundMode(lv_event_t* e) {
|
|
s_ui->setSoundMode((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onMelodyRow(lv_event_t* e) { s_ui->showMelodies((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onMelSlot(lv_event_t* e) {
|
|
s_ui->melodySlot((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onMelNote(lv_event_t* e) { s_ui->melodyPick((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onMelMatrix(lv_event_t* e) {
|
|
s_ui->melodySet((uint8_t)(uintptr_t)lv_event_get_user_data(e),
|
|
(int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onMelBpm(lv_event_t* e) {
|
|
s_ui->melodySet(MS_BPM, (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onMelAction(lv_event_t* e) { s_ui->melodyAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onMelPlay(lv_event_t* e) { (void)e; s_ui->melodyAction(MA_PLAY); }
|
|
|
|
static lv_obj_t* smallButton(lv_obj_t* parent, const char* text, uint8_t act) {
|
|
lv_obj_t* b = lv_button_create(parent);
|
|
lv_obj_set_size(b, 40, 32);
|
|
lv_obj_set_style_pad_all(b, 0, 0);
|
|
lv_obj_set_style_radius(b, 8, 0);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
|
|
lv_obj_add_event_cb(b, onMelAction, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
|
|
lv_obj_center(label(b, text, THEME_FONT_BODY, theme::TEXT));
|
|
return b;
|
|
}
|
|
|
|
static lv_obj_t* flexRow(lv_obj_t* parent) {
|
|
lv_obj_t* r = lv_obj_create(parent);
|
|
styleSurface(r, theme::BG);
|
|
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(r, theme::GAP, 0);
|
|
return r;
|
|
}
|
|
|
|
} // namespace sndview
|
|
|
|
// ── The speaker ───────────────────────────────────────────────────────────────
|
|
|
|
void UITask::playMelody(const char* melody) {
|
|
#ifdef PIN_BUZZER
|
|
_buzzer.playForced(melody);
|
|
#else
|
|
(void)melody;
|
|
#endif
|
|
}
|
|
|
|
void UITask::stopMelody() {
|
|
#ifdef PIN_BUZZER
|
|
_buzzer.stop();
|
|
#endif
|
|
}
|
|
|
|
int UITask::melodyNote() {
|
|
#ifdef PIN_BUZZER
|
|
return _buzzer.noteIndex();
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
bool UITask::melodyPlaying() {
|
|
#ifdef PIN_BUZZER
|
|
return _buzzer.isPlaying();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#ifdef PIN_BUZZER
|
|
uint16_t UITask::buzzerFreqHz() const {
|
|
#ifdef SIM_PLATFORM
|
|
return _buzzer.currentFreqHz();
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// An incoming message, advert or delivery tick: the sound its settings pick.
|
|
void UITask::notify(UIEventType t) {
|
|
#ifdef PIN_BUZZER
|
|
SoundNotifier sn(_buzzer, _prefs, _notif_mel_buf, sizeof(_notif_mel_buf));
|
|
switch (t) {
|
|
case UIEventType::contactMessage:
|
|
sn.playDM(_notif_dm_valid, _notif_dm_prefix);
|
|
_notif_dm_valid = false;
|
|
break;
|
|
case UIEventType::channelMessage:
|
|
sn.playCH(_notif_ch_idx);
|
|
_notif_ch_idx = -1;
|
|
break;
|
|
case UIEventType::roomMessage: // many authors, no per-room melody: the DM default
|
|
sn.playDM(false, nullptr);
|
|
break;
|
|
case UIEventType::advertReceivedFlood:
|
|
case UIEventType::advertReceivedZeroHop:
|
|
sn.playAD(t == UIEventType::advertReceivedFlood);
|
|
break;
|
|
case UIEventType::ack:
|
|
_buzzer.play("ack:d=32,o=8,b=120:c");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
#else
|
|
(void)t;
|
|
#endif
|
|
}
|
|
|
|
// ── Settings > Sound ──────────────────────────────────────────────────────────
|
|
|
|
void UITask::applySoundPrefs() {
|
|
#ifdef PIN_BUZZER
|
|
if (_prefs) _buzzer.setVolume(_prefs->buzzer_volume);
|
|
#endif
|
|
}
|
|
|
|
void UITask::setSoundMode(int mode) {
|
|
if (!_prefs || mode < 0 || mode >= soundctl::MODE_COUNT) return;
|
|
#ifdef PIN_BUZZER
|
|
soundctl::setMode(_prefs, _buzzer, (uint8_t)mode, isClientConnected());
|
|
if (mode == soundctl::MODE_ON) notify(UIEventType::ack); // hear that it's on
|
|
#endif
|
|
the_mesh.savePrefs();
|
|
refreshStatusBar();
|
|
if (mode == soundctl::MODE_AUTO) showToast(isClientConnected() ? "Silent while the app is connected" : "Sound on until the app connects");
|
|
}
|
|
|
|
void UITask::setSoundVolume(int level) {
|
|
if (!_prefs) return;
|
|
#ifdef PIN_BUZZER
|
|
soundctl::setVolume(_prefs, _buzzer, (uint8_t)level);
|
|
#endif
|
|
the_mesh.savePrefs();
|
|
}
|
|
|
|
// The Sound page's own rows: the mode at the top, the melodies at the bottom.
|
|
void UITask::buildSoundRows(lv_obj_t* body, bool top) {
|
|
using namespace sndview;
|
|
if (top) {
|
|
static const char* MODES[] = { "On", "Off", "Auto", "" };
|
|
lv_obj_t* row = radioview::settingRow(body, "Sound", "Auto: off with the app");
|
|
radioview::rowSegmented(row, MODES, soundctl::mode(_prefs), 150, onSoundMode, 0);
|
|
return;
|
|
}
|
|
sectionTitle(body, "MELODIES");
|
|
for (int slot = 0; slot < 2; slot++) {
|
|
soundctl::Melody m;
|
|
soundctl::load(_prefs, slot, m);
|
|
char sub[40];
|
|
if (m.len) snprintf(sub, sizeof(sub), "%d note%s, %u BPM", m.len, m.len == 1 ? "" : "s", soundctl::bpm(m.bpm_idx));
|
|
else snprintf(sub, sizeof(sub), "Empty - tap to compose");
|
|
listRow(body, slot ? LV_SYMBOL_AUDIO " Melody 2" : LV_SYMBOL_AUDIO " Melody 1", sub, onMelodyRow, (void*)(uintptr_t)slot);
|
|
}
|
|
lv_obj_t* t = label(body, "Pick them above for each kind of message, or for one chat in its options (hold it in Chats).",
|
|
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(t, LV_PCT(100));
|
|
}
|
|
|
|
// ── Melody editor ─────────────────────────────────────────────────────────────
|
|
|
|
void UITask::showMelodies(int slot) {
|
|
using namespace sndview;
|
|
stopMelody();
|
|
s_slot = slot == 1 ? 1 : 0;
|
|
soundctl::load(_prefs, s_slot, s_mel);
|
|
s_sel = 0;
|
|
s_dirty = false;
|
|
s_play_all = false;
|
|
_screen = SCR_MELODY;
|
|
buildMelodies();
|
|
}
|
|
|
|
void UITask::buildMelodies() {
|
|
using namespace sndview;
|
|
lv_obj_t* body = newScreen("Melodies", true);
|
|
s_title = lv_obj_get_child(_header, 1); // after the back button
|
|
lv_obj_set_style_pad_row(body, 6, 0);
|
|
headerButton(_header, LV_SYMBOL_PLAY " Play", onMelPlay, 0, &s_play_lbl);
|
|
s_playing_shown = false;
|
|
|
|
lv_obj_t* top = flexRow(body);
|
|
lv_obj_t* slots = segmented(top, SLOTS, s_slot, 150, 32);
|
|
lv_obj_add_event_cb(slots, onMelSlot, LV_EVENT_VALUE_CHANGED, NULL);
|
|
s_bpm = lv_dropdown_create(top);
|
|
lv_dropdown_set_options_static(s_bpm, "60 BPM\n90 BPM\n120 BPM\n150 BPM\n180 BPM");
|
|
lv_dropdown_set_selected(s_bpm, s_mel.bpm_idx);
|
|
lv_obj_set_size(s_bpm, 102, 32);
|
|
lv_obj_set_style_pad_ver(s_bpm, 6, 0);
|
|
lv_obj_add_event_cb(s_bpm, onMelBpm, LV_EVENT_VALUE_CHANGED, NULL);
|
|
smallButton(top, LV_SYMBOL_PLUS, MA_ADD); // (fits: 150 + 102 + 40 + gaps)
|
|
|
|
s_strip = lv_obj_create(body);
|
|
styleSurface(s_strip, theme::BG);
|
|
lv_obj_set_size(s_strip, LV_PCT(100), 50);
|
|
lv_obj_set_flex_flow(s_strip, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_column(s_strip, 4, 0);
|
|
lv_obj_set_style_pad_ver(s_strip, 3, 0);
|
|
lv_obj_set_scroll_dir(s_strip, LV_DIR_HOR);
|
|
lv_obj_set_scrollbar_mode(s_strip, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
s_pitch = segmented(body, PITCHES, -1, LV_PCT(100), 34);
|
|
lv_obj_add_event_cb(s_pitch, onMelMatrix, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)MS_PITCH);
|
|
|
|
lv_obj_t* bottom = flexRow(body);
|
|
s_oct = segmented(bottom, OCTAVES, -1, 84, 32);
|
|
lv_obj_add_event_cb(s_oct, onMelMatrix, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)MS_OCT);
|
|
s_dur = segmented(bottom, DURS, -1, 158, 32);
|
|
lv_obj_add_event_cb(s_dur, onMelMatrix, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)MS_DUR);
|
|
smallButton(bottom, LV_SYMBOL_TRASH, MA_DELETE);
|
|
for (lv_obj_t* m : { s_pitch, s_oct, s_dur }) { // greyed out with no note to edit
|
|
lv_obj_set_style_text_opa(m, LV_OPA_30, LV_PART_ITEMS | LV_STATE_DISABLED);
|
|
lv_obj_set_style_bg_color(m, lv_color_hex(theme::SURFACE), LV_PART_ITEMS | LV_STATE_DISABLED);
|
|
}
|
|
|
|
rebuildStrip();
|
|
syncControls();
|
|
}
|
|
|
|
// Every 30 ms on SCR_MELODY: the playing note lights up, the header's
|
|
// Play / Stop follows the player.
|
|
void UITask::refreshMelody() {
|
|
using namespace sndview;
|
|
bool playing = melodyPlaying();
|
|
if (!playing) s_play_all = false;
|
|
lightNote(s_play_all ? melodyNote() : -1);
|
|
if (!s_play_lbl || playing == s_playing_shown) return;
|
|
s_playing_shown = playing;
|
|
lv_label_set_text(s_play_lbl, playing ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Play");
|
|
}
|
|
|
|
void UITask::melodySave() {
|
|
using namespace sndview;
|
|
if (!s_dirty || !_prefs) return;
|
|
soundctl::store(_prefs, s_slot, s_mel);
|
|
the_mesh.savePrefs();
|
|
s_dirty = false;
|
|
showToast(s_slot ? "Melody 2 saved" : "Melody 1 saved", 1200);
|
|
}
|
|
|
|
void UITask::melodySlot(int slot) {
|
|
using namespace sndview;
|
|
if (slot == s_slot) return;
|
|
melodySave();
|
|
stopMelody();
|
|
s_play_all = false;
|
|
s_slot = slot == 1 ? 1 : 0;
|
|
soundctl::load(_prefs, s_slot, s_mel);
|
|
s_sel = 0;
|
|
if (s_bpm) lv_dropdown_set_selected(s_bpm, s_mel.bpm_idx);
|
|
rebuildStrip();
|
|
syncControls();
|
|
refreshMelody();
|
|
}
|
|
|
|
void UITask::melodyPick(int idx) {
|
|
using namespace sndview;
|
|
if (idx < 0 || idx >= s_mel.len) return;
|
|
styleCell(s_sel, false);
|
|
s_sel = idx;
|
|
styleCell(s_sel, true);
|
|
syncControls();
|
|
preview(this, s_mel.notes[s_sel]);
|
|
}
|
|
|
|
void UITask::melodySet(uint8_t which, int v) {
|
|
using namespace sndview;
|
|
if (which == MS_BPM) {
|
|
if (v < 0 || v >= soundctl::BPM_COUNT) return;
|
|
s_mel.bpm_idx = (uint8_t)v;
|
|
s_dirty = true;
|
|
return;
|
|
}
|
|
if (s_sel >= s_mel.len || v < 0) return;
|
|
uint8_t b = s_mel.notes[s_sel];
|
|
uint8_t p = soundctl::notePitch(b), o = soundctl::noteOctave(b), d = soundctl::noteDur(b);
|
|
if (which == MS_PITCH && v < soundctl::PITCH_COUNT) p = (uint8_t)v;
|
|
else if (which == MS_OCT && v <= soundctl::OCT_MAX - soundctl::OCT_MIN) o = (uint8_t)(soundctl::OCT_MIN + v);
|
|
else if (which == MS_DUR && v < soundctl::DUR_COUNT) d = (uint8_t)v;
|
|
else return;
|
|
s_mel.notes[s_sel] = soundctl::packNote(p, o, d);
|
|
s_dirty = true;
|
|
cellText(s_sel);
|
|
syncControls();
|
|
if (which != MS_DUR) preview(this, s_mel.notes[s_sel]);
|
|
}
|
|
|
|
void UITask::melodyAction(uint8_t act) {
|
|
using namespace sndview;
|
|
switch (act) {
|
|
case MA_PLAY:
|
|
if (melodyPlaying()) { stopMelody(); s_play_all = false; break; }
|
|
if (!s_mel.len) { showToast("Add a note first", 1200); break; }
|
|
soundctl::toRtttl(s_mel, s_play_buf, sizeof(s_play_buf));
|
|
playMelody(s_play_buf);
|
|
s_play_all = true;
|
|
break;
|
|
case MA_ADD: { // a copy of the selected note after it (or C5 1/8 to start)
|
|
int at = s_mel.len ? s_sel + 1 : 0;
|
|
uint8_t note = s_mel.len ? s_mel.notes[s_sel] : soundctl::NEW_NOTE;
|
|
if (!soundctl::insertNote(s_mel, at, note)) { showToast("32 notes at most", 1200); break; }
|
|
s_sel = at;
|
|
s_dirty = true;
|
|
rebuildStrip();
|
|
syncControls();
|
|
preview(this, note);
|
|
break;
|
|
}
|
|
case MA_DELETE:
|
|
if (!soundctl::removeNote(s_mel, s_sel)) break;
|
|
if (s_sel >= s_mel.len && s_sel > 0) s_sel--;
|
|
s_dirty = true;
|
|
rebuildStrip();
|
|
syncControls();
|
|
break;
|
|
}
|
|
refreshMelody();
|
|
}
|
|
|
|
// ── A conversation's melody (its options popup) ───────────────────────────────
|
|
|
|
void UITask::conversationMelody(int v) {
|
|
if (!_prefs || v < 0 || v >= soundctl::OVERRIDE_COUNT) return;
|
|
contactctl::setMelody(_prefs, convview::s_key, (uint8_t)v);
|
|
the_mesh.savePrefs();
|
|
hearMelody(v);
|
|
}
|
|
|
|
// A melody picked for a chat: play it (slot 1 / 2; 0 = the default, silent).
|
|
void UITask::hearMelody(int slot) {
|
|
if (slot < 1 || slot > 2) return;
|
|
soundctl::Melody m;
|
|
soundctl::load(_prefs, slot - 1, m);
|
|
if (!m.len) { showToast("That melody is empty - compose it in Settings > Sound", 2500); return; }
|
|
stopMelody();
|
|
soundctl::toRtttl(m, sndview::s_play_buf, sizeof(sndview::s_play_buf));
|
|
playMelody(sndview::s_play_buf);
|
|
}
|