/* Copyright (C) 2026 Reiner Herrmann * SPDX-License-Identifier: GPL-3.0-or-later */ #include static Window *metronome_window; static Layer *draw_layer; static TextLayer *bpm_text; static AppTimer *beat_timer; #define BPM_TO_MS(bpm) (60000/(bpm)) #define IS_FIRST_BEAT() (current_beat % beats == 0) #define COLOR_BG GColorWhite #define COLOR_BG_INV GColorBlack #define COLOR_FG GColorBlack #define NOTE_C4 60 #define NOTE_C5 72 static int bpm = 90; static int beats = 4; static int current_beat = -1; static bool playing = true; static bool flash_enabled = true; static bool vibrate_enabled = true; static bool beep_enabled = true; static void restore_background_color() { window_set_background_color(metronome_window, COLOR_BG); } static void metronome_flash() { static const int flash_duration = 50; if (!flash_enabled) { return; } window_set_background_color(metronome_window, COLOR_BG_INV); app_timer_register(flash_duration, restore_background_color, NULL); } static void metronome_vibrate() { if (!vibrate_enabled) { return; } if (IS_FIRST_BEAT()) { // TODO: slightly longer pulse vibes_short_pulse(); } else { vibes_short_pulse(); } } static void metronome_beep() { #if defined(PBL_SPEAKER) if (!beep_enabled || speaker_is_muted()) { return; } const SpeakerNote note = { .midi_note = IS_FIRST_BEAT() ? NOTE_C5 : NOTE_C4, .waveform = SpeakerWaveformSquare, .duration_ms = 150, .velocity = 0, }; speaker_play_notes(¬e, 1, 0); #endif } static void beat_timer_callback() { beat_timer = app_timer_register(BPM_TO_MS(bpm), beat_timer_callback, NULL); current_beat++; metronome_flash(); metronome_vibrate(); metronome_beep(); layer_mark_dirty(draw_layer); } static void update_bpm_text() { static char bpm_buf[6]; snprintf(bpm_buf, sizeof(bpm_buf), "%d", bpm); text_layer_set_text(bpm_text, bpm_buf); } static void beat_indicators_update_proc(Layer *layer, GContext *ctx) { GRect bounds = layer_get_unobstructed_bounds(layer); const int rect_radius = 10; const int width = bounds.size.w / beats; const int height = bounds.size.h; for (int i=0; i= beats) { color1 = GColorGreen; color2 = GColorOrange; } else { color1 = GColorOrange; color2 = GColorGreen; } GRect rect = GRect(5 + (i * width), 10, width - 10, height - 10); if (current_beat % beats < i) { graphics_context_set_fill_color(ctx, color1); } else { graphics_context_set_fill_color(ctx, color2); } graphics_fill_rect(ctx, rect, rect_radius, GCornersAll); graphics_context_set_stroke_color(ctx, COLOR_FG); graphics_draw_round_rect(ctx, rect, rect_radius); } } bool is_paused() { return !playing; } void start_metronome() { beat_timer = app_timer_register(0, beat_timer_callback, NULL); playing = true; } void stop_metronome() { app_timer_cancel(beat_timer); playing = false; } static void metronome_window_load(Window *window) { Layer *window_layer = window_get_root_layer(window); GRect bounds = layer_get_unobstructed_bounds(window_layer); const int font_height = 38; /* layer for drawing beat bars */ draw_layer = layer_create(GRect(0, 0, bounds.size.w - ACTION_BAR_WIDTH, bounds.size.h - font_height - 2*20)); layer_set_update_proc(draw_layer, beat_indicators_update_proc); layer_add_child(window_layer, draw_layer); /* BPM text */ bpm_text = text_layer_create(GRect(bounds.origin.x, bounds.size.h - font_height - 20, bounds.size.w - ACTION_BAR_WIDTH, font_height)); text_layer_set_background_color(bpm_text, GColorClear); text_layer_set_text_color(bpm_text, COLOR_FG); text_layer_set_text_alignment(bpm_text, GTextAlignmentCenter); text_layer_set_font(bpm_text, fonts_get_system_font(FONT_KEY_LECO_38_BOLD_NUMBERS)); update_bpm_text(); layer_add_child(window_layer, text_layer_get_layer(bpm_text)); start_metronome(); } static void metronome_window_unload(Window *window) { text_layer_destroy(bpm_text); } Window *create_metronome_window() { metronome_window = window_create(); window_set_window_handlers(metronome_window, (WindowHandlers) { .load = metronome_window_load, .unload = metronome_window_unload, }); window_set_background_color(metronome_window, COLOR_BG); return metronome_window; } void increase_bpm() { bpm++; if (bpm > 600) { bpm = 600; } APP_LOG(APP_LOG_LEVEL_DEBUG, "increasing bpm to: %d", bpm); update_bpm_text(); } void decrease_bpm() { bpm--; if (bpm <= 0) { bpm = 0; } APP_LOG(APP_LOG_LEVEL_DEBUG, "decreasing bpm to: %d", bpm); update_bpm_text(); } void increase_beats() { beats++; if (beats > 16) { beats = 16; } APP_LOG(APP_LOG_LEVEL_DEBUG, "increasing beats to: %d", beats); } void decrease_beats() { beats--; if (beats < 1) { beats = 1; } APP_LOG(APP_LOG_LEVEL_DEBUG, "decreasing beats to: %d", beats); }