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
2 changes: 2 additions & 0 deletions boards/asterix/defconfig
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ CONFIG_RNG_STUB=y

CONFIG_ACCEL_SENSITIVITY=y
CONFIG_ORIENTATION_MANAGER=y
# 1 MB code flash cannot fit the sampled sound pack
# CONFIG_NOTIFICATION_SOUND_SAMPLES is not set
2 changes: 2 additions & 0 deletions boards/getafix/defconfig
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ CONFIG_ACCEL_SENSITIVITY=y
CONFIG_APP_SCALING=y
CONFIG_ORIENTATION_MANAGER=y
CONFIG_MODDABLE_XS=y
# 1 MB code flash cannot fit the sampled sound pack
# CONFIG_NOTIFICATION_SOUND_SAMPLES is not set
6 changes: 6 additions & 0 deletions include/pbl/services/notifications/alerts.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ bool alerts_should_enable_backlight_for_type(AlertType type);

bool alerts_should_vibrate_for_type(AlertType type);

//! Whether the notification sound should play for this alert. Master alert
//! gating and DND follow the vibe rules, but the on-charger suppression does
//! not apply: a docked watch can still usefully chirp. Speaker mute and
//! volume are enforced downstream by the speaker service.
bool alerts_should_play_sound_for_type(AlertType type);

//! When vibrating for an incoming notification, call this function to prevent multiple vibes
//! within a short period of time.
void alerts_set_notification_vibe_timestamp();
9 changes: 9 additions & 0 deletions include/pbl/services/notifications/alerts_preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <stdint.h>

#include "alerts_private.h"
#include "pbl/services/notifications/notification_sounds.h"

typedef enum FirstUseSource {
FirstUseSourceManualDNDActionMenu = 0,
Expand Down Expand Up @@ -79,6 +80,14 @@ void alerts_preferences_set_speaker_volume(uint8_t volume);
//! @return The system-wide speaker volume cap (0-100). Defaults to 100.
uint8_t alerts_preferences_get_speaker_volume(void);

//! Set the sound played when a notification arrives.
void alerts_preferences_set_notification_sound(NotificationSound sound);

//! @return The sound played when a notification arrives. Defaults to
//! NotificationSound_None (silent); out-of-range stored values also read
//! back as None.
NotificationSound alerts_preferences_get_notification_sound(void);

//! Checks whether a given "first use" dialog has been shown and sets it as complete
//! @param source The "first use" bit to check
//! @return true if the dialog has already been shown, false otherwise
Expand Down
43 changes: 43 additions & 0 deletions include/pbl/services/notifications/notification_sounds.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2026 Core Devices LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#pragma once

#include "pbl/services/speaker/note_sequence.h"

#include <stdint.h>

//! Short chirps played when a notification arrives, deliberately briefer than
//! the alarm tones: a notification sound must not outstay the vibe.
typedef enum NotificationSound {
NotificationSound_None = 0,
NotificationSound_Ping,
NotificationSound_Doorbell,
NotificationSound_Trill,
NotificationSound_Ascent,
NotificationSound_Glass, // replaced Bell; existing stored prefs map here
NotificationSound_Pop,
NotificationSound_Kalimba,
NotificationSound_Coin,
NotificationSound_Sonar,
NotificationSound_Bounce,
NotificationSound_Chirp,
NotificationSound_Bloom,
NotificationSound_Sparkle,
NotificationSound_Count,
} NotificationSound;

//! Play a notification sound on the speaker at the given volume. Synthesized
//! sounds go through the note-sequence source; sampled sounds (e.g. Bell)
//! through the track player. Out-of-range values fall back to Ping;
//! NotificationSound_None is a no-op.
//! @return true if playback started.
bool notification_sounds_play(NotificationSound sound, uint8_t volume);

//! Get the i18n_noop()'d display name for a sound (including "Off" for None).
//! Caller wraps with i18n_get() at display time.
const char *notification_sounds_get_name(NotificationSound sound);

//! Next sound in the cycle order for the settings UI, wrapping from the last
//! sound back to None.
NotificationSound notification_sounds_cycle_next(NotificationSound sound);
31 changes: 31 additions & 0 deletions src/fw/apps/system/settings/notifications.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,23 @@
#include "applib/ui/ui.h"
#include "kernel/pbl_malloc.h"
#include "pbl/services/i18n/i18n.h"
#include "pbl/services/notifications/alerts_preferences.h"
#include "pbl/services/notifications/alerts_preferences_private.h"
#include "pbl/services/notifications/alerts_private.h"
#include "pbl/services/notifications/notification_sounds.h"
#include "system/passert.h"
#include "pbl/util/size.h"
#include "util/time/time.h"

// Offset between vibe intensity menu item index and vibe intensity enum values
#define INTENSITY_ROW_OFFSET 1

#ifdef CONFIG_SPEAKER
//! Matches NOTIFICATION_SOUND_VOLUME in notification_window.c so the preview
//! is heard at the volume the real notification will use.
#define NOTIFICATION_SOUND_PREVIEW_VOLUME 50
#endif

typedef struct {
SettingsCallbacks callbacks;
EventServiceInfo battery_connection_event_info;
Expand All @@ -33,6 +41,9 @@ enum NotificationsItem {
NotificationsItemWindowTimeout,
#if PBL_BW
NotificationsItemDesignStyle,
#endif
#ifdef CONFIG_SPEAKER
NotificationsItemSound,
#endif
NotificationsItemVibeDelay,
NotificationsItemBacklight,
Expand Down Expand Up @@ -328,6 +339,14 @@ static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx, const Lay
break;
}
#endif /* PBL_BW */
#ifdef CONFIG_SPEAKER
case NotificationsItemSound: {
/// String within Settings->Notifications that selects the notification sound
title = i18n_noop("Sound");
subtitle = notification_sounds_get_name(alerts_preferences_get_notification_sound());
break;
}
#endif /* CONFIG_SPEAKER */
case NotificationsItemVibeDelay: {
/// String within Settings->Notifications that describes when vibration happens
title = i18n_noop("Vibe Timing");
Expand Down Expand Up @@ -378,6 +397,18 @@ static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) {
prv_design_style_menu_push(data);
break;
#endif /* PBL_BW */
#ifdef CONFIG_SPEAKER
case NotificationsItemSound: {
const NotificationSound sound =
notification_sounds_cycle_next(alerts_preferences_get_notification_sound());
alerts_preferences_set_notification_sound(sound);
if (sound != NotificationSound_None) {
// Preview the newly selected sound
notification_sounds_play(sound, NOTIFICATION_SOUND_PREVIEW_VOLUME);
}
break;
}
#endif /* CONFIG_SPEAKER */
case NotificationsItemVibeDelay:
prv_vibe_delay_menu_push(data);
break;
Expand Down
2 changes: 1 addition & 1 deletion src/fw/apps/system/settings/vibe_patterns.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ static void prv_selection_changed_cb(SettingsCallbacks *context, uint16_t new_ro
#ifdef CONFIG_SPEAKER
case VibeSettingsRow_MuteSpeaker:
case VibeSettingsRow_SpeakerVolume: {
// No vibe preview — this row controls a non-vibe setting.
// No vibe preview — these rows control non-vibe settings.
return;
}
#endif
Expand Down
44 changes: 43 additions & 1 deletion src/fw/popups/notifications/notification_window.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "pbl/services/blob_db/reminder_db.h"
#include "pbl/services/notifications/alerts.h"
#include "pbl/services/notifications/alerts_preferences.h"
#include "pbl/services/notifications/notification_sounds.h"
#include "pbl/services/notifications/alerts_preferences_private.h"
#include "pbl/services/notifications/alerts_private.h"
#include "pbl/services/notifications/ancs/ancs_filtering.h"
Expand Down Expand Up @@ -84,6 +85,7 @@ static void prv_handle_notification_removed_common(Uuid *, NotificationType);
static bool prv_should_pop_due_to_inactivity(void);

static void prv_do_notification_vibe(NotificationWindowData *data, Uuid *id);
static void prv_do_notification_sound(void);

/////////////////////
// Helpers
Expand Down Expand Up @@ -302,6 +304,10 @@ static void prv_peek_anim_stopped(Animation *animation, bool finished, void *con
data->pending_vibe = false;
prv_do_notification_vibe(data, &data->pending_vibe_id);
}
if (data->pending_sound) {
data->pending_sound = false;
prv_do_notification_sound();
}
if (data->pending_backlight) {
data->pending_backlight = false;
if (!data->color_preempted) {
Expand All @@ -321,6 +327,10 @@ static void prv_hide_peek_layer(void *context) {
data->pending_vibe = false;
prv_do_notification_vibe(data, &data->pending_vibe_id);
}
if (data->pending_sound) {
data->pending_sound = false;
prv_do_notification_sound();
}
if (data->pending_backlight) {
data->pending_backlight = false;
if (!data->color_preempted) {
Expand Down Expand Up @@ -1101,6 +1111,7 @@ static void prv_window_unload(Window *window) {

vibes_cancel();
data->pending_vibe = false;
data->pending_sound = false;
if (data->color_preempted) {
data->color_preempted = false;
light_system_color_release();
Expand Down Expand Up @@ -1290,6 +1301,7 @@ static void prv_init_notification_window(bool is_modal) {
data->action_menu = NULL;
data->dnd_icon_visible = false;
data->pending_vibe = false;
data->pending_sound = false;

Window *window = &data->window;
window_init(window, "Notification Window");
Expand Down Expand Up @@ -1486,6 +1498,20 @@ static void prv_handle_notification_acted_upon(Uuid *id) {
}
}

//! Notification chirps play noticeably quieter than alarms: they announce,
//! they don't wake.
#define NOTIFICATION_SOUND_VOLUME 50

static void prv_do_notification_sound(void) {
if (notification_sounds_play(alerts_preferences_get_notification_sound(),
NOTIFICATION_SOUND_VOLUME)) {
// Stamp the shared holdoff clock so sound-only setups (vibe disabled)
// still get storm throttling. Callers evaluate both alert gates before
// firing either, so this cannot suppress this notification's own vibe.
alerts_set_notification_vibe_timestamp();
}
}

static void prv_do_notification_vibe(NotificationWindowData *data, Uuid *id) {
PBL_LOG_DBG("Notification vibe: do_vibe called");
TimelineItem *item = prv_get_current_notification(data);
Expand Down Expand Up @@ -1581,7 +1607,14 @@ static void prv_handle_notification_added_common(Uuid *id, NotificationType type
}
}

if (alerts_should_vibrate_for_type(prv_alert_type_for_notification_type(type))) {
// Evaluate both gates before acting on either: firing the vibe (or sound)
// stamps the shared holdoff timestamp, which would otherwise suppress the
// other alert for this same notification.
const AlertType alert_type = prv_alert_type_for_notification_type(type);
const bool should_vibe = alerts_should_vibrate_for_type(alert_type);
const bool should_sound = alerts_should_play_sound_for_type(alert_type);

if (should_vibe) {
// Check if we should delay the vibration until the animation completes
if (alerts_preferences_get_notification_vibe_delay() && data->peek_layer) {
// Delay vibration until peek animation finishes
Expand All @@ -1593,6 +1626,15 @@ static void prv_handle_notification_added_common(Uuid *id, NotificationType type
}
}

if (should_sound) {
// Sound follows the same delay-until-peek-settles choice as the vibe
if (alerts_preferences_get_notification_vibe_delay() && data->peek_layer) {
data->pending_sound = true;
} else {
prv_do_notification_sound();
}
}

if (alerts_should_enable_backlight_for_type(prv_alert_type_for_notification_type(type))) {
// Check if we should delay the backlight until the animation completes (same as vibe delay)
if (alerts_preferences_get_notification_vibe_delay() && data->peek_layer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ typedef struct NotificationWindowData {
bool pending_vibe;
Uuid pending_vibe_id;
bool pending_backlight;
bool pending_sound;

// Set once we've pushed a modal color preempt for this notification's
// backlight pulse, so window_unload knows to balance it with a pop.
Expand Down
1 change: 1 addition & 0 deletions src/fw/services/notifications/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ set(sources
do_not_disturb.c
do_not_disturb_toggle.c
notification_image.c
notification_sounds.c
notification_storage.c
notifications.c
)
Expand Down
9 changes: 9 additions & 0 deletions src/fw/services/notifications/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ config SERVICE_NOTIFICATIONS

if SERVICE_NOTIFICATIONS

config NOTIFICATION_SOUND_SAMPLES
bool "Sampled notification sounds"
depends on SPEAKER
default y
help
Include the sampled (PCM) notification sounds. Costs ~55 KB of code
flash; boards with a 1 MB code budget keep only the synthesized
chirps.

module = SERVICE_NOTIFICATIONS
module-str = Notifications
source "subsys/logging/Kconfig.template.log_level"
Expand Down
23 changes: 23 additions & 0 deletions src/fw/services/notifications/alerts.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ bool alerts_should_vibrate_for_type(AlertType type) {
return alerts_preferences_get_vibrate();
}

bool alerts_should_play_sound_for_type(AlertType type) {
if (alerts_preferences_get_notification_sound() == NotificationSound_None) {
return false;
}

if (do_not_disturb_is_active() && !(alerts_preferences_dnd_get_mask() & type)) {
return false;
}

if (!alerts_should_notify_for_type(type)) {
return false;
}

// Reuse the vibe holdoff so a notification storm doesn't chirp continuously.
// Unlike vibes, sounds still play on the charger: the watch may be off-wrist
// where a vibe would go unnoticed but a chirp would not.
if (prv_get_ms_since_last_notification_vibe() < NOTIFICATION_VIBE_HOLDOFF_MS) {
return false;
}

return true;
}

bool alerts_get_vibrate(void) {
return alerts_preferences_get_vibrate();
}
Expand Down
16 changes: 16 additions & 0 deletions src/fw/services/notifications/alerts_preferences.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ static bool s_speaker_muted = false;
#define PREF_KEY_SPEAKER_VOLUME "speakerVolume"
static uint8_t s_speaker_volume = 100;

#define PREF_KEY_NOTIFICATION_SOUND "notificationSound"
static uint8_t s_notification_sound = NotificationSound_None;

#define PREF_KEY_VIBE "vibe"
static bool s_vibe_on_notification = true;

Expand Down Expand Up @@ -327,6 +330,7 @@ void alerts_preferences_init(void) {
RESTORE_PREF(PREF_KEY_DND_MUTE_SPEAKER, s_dnd_mute_speaker);
RESTORE_PREF(PREF_KEY_SPEAKER_MUTED, s_speaker_muted);
RESTORE_PREF(PREF_KEY_SPEAKER_VOLUME, s_speaker_volume);
RESTORE_PREF(PREF_KEY_NOTIFICATION_SOUND, s_notification_sound);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE, s_legacy_dnd_schedule);
RESTORE_PREF(PREF_KEY_LEGACY_DND_SCHEDULE_ENABLED, s_legacy_dnd_schedule_enabled);
RESTORE_PREF(s_dnd_schedule_keys[WeekdaySchedule].schedule_pref_key,
Expand Down Expand Up @@ -478,6 +482,18 @@ void alerts_preferences_set_speaker_volume(uint8_t volume) {
SET_PREF(PREF_KEY_SPEAKER_VOLUME, s_speaker_volume);
}

NotificationSound alerts_preferences_get_notification_sound(void) {
if (s_notification_sound >= NotificationSound_Count) {
return NotificationSound_None;
}
return (NotificationSound)s_notification_sound;
}

void alerts_preferences_set_notification_sound(NotificationSound sound) {
s_notification_sound = sound;
SET_PREF(PREF_KEY_NOTIFICATION_SOUND, s_notification_sound);
}

///////////////////////////////////////////////////////////////////////////////////////////////////
//! Vibes

Expand Down
Loading