From df2fbf4c9291cce340945a0a71de0200ccae7369 Mon Sep 17 00:00:00 2001 From: Reiner Herrmann Date: Wed, 8 Jul 2026 00:23:22 +0200 Subject: initial commit --- src/c/app.c | 114 ++++++++++++++++++++++++++++++ src/c/metronome.c | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/c/metronome.h | 21 ++++++ 3 files changed, 340 insertions(+) create mode 100644 src/c/app.c create mode 100644 src/c/metronome.c create mode 100644 src/c/metronome.h (limited to 'src') diff --git a/src/c/app.c b/src/c/app.c new file mode 100644 index 0000000..2ce9996 --- /dev/null +++ b/src/c/app.c @@ -0,0 +1,114 @@ +/* Copyright (C) 2026 Reiner Herrmann + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include "metronome.h" + +static Window *app_window; +static ActionBarLayer *action_bar; + +static GBitmap *icon_left_arrows; +static GBitmap *icon_right_arrows; +static GBitmap *icon_plus; +static GBitmap *icon_minus; +static GBitmap *icon_three_dots; +static GBitmap *icon_pause; +static GBitmap *icon_play; + +static AppTimer *action_bar_timer; + +static const int action_bar_timeout = 2000; + +static void primary_action_bar(); +static void secondary_action_bar(); + +static void increase_bpm_handler(ClickRecognizerRef recognizer, void *context) { + increase_bpm(); +} + +static void decrease_bpm_handler(ClickRecognizerRef recognizer, void *context) { + decrease_bpm(); +} + +static void increase_beats_handler(ClickRecognizerRef recognizer, void *context) { + increase_beats(); + app_timer_reschedule(action_bar_timer, action_bar_timeout); +} + +static void decrease_beats_handler(ClickRecognizerRef recognizer, void *context) { + decrease_beats(); + app_timer_reschedule(action_bar_timer, action_bar_timeout); +} + +static void switch_to_secondary_handler(ClickRecognizerRef recognizer, void *context) { + secondary_action_bar(); + + /* switch back to primary after timeout */ + action_bar_timer = app_timer_register(action_bar_timeout, primary_action_bar, NULL); +} + +static void pause_play_handler(ClickRecognizerRef recognizer, void *context) { + if (is_paused()) { + start_metronome(); + action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, icon_pause); + } else { + stop_metronome(); + action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, icon_play); + } + app_timer_reschedule(action_bar_timer, action_bar_timeout); +} + +static void click_config_provider_primary(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, increase_bpm_handler); + window_single_click_subscribe(BUTTON_ID_SELECT, switch_to_secondary_handler); + window_single_click_subscribe(BUTTON_ID_DOWN, decrease_bpm_handler); +} + +static void click_config_provider_secondary(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, increase_beats_handler); + window_single_click_subscribe(BUTTON_ID_SELECT, pause_play_handler); + window_single_click_subscribe(BUTTON_ID_DOWN, decrease_beats_handler); +} + +static void secondary_action_bar() { + action_bar_layer_set_click_config_provider(action_bar, click_config_provider_secondary); + action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, icon_plus); + action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, icon_minus); + action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, is_paused() ? icon_play : icon_pause); +} + +static void primary_action_bar() { + action_bar_layer_set_click_config_provider(action_bar, click_config_provider_primary); + action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, icon_right_arrows); + action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, icon_left_arrows); + action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, icon_three_dots); +} + +static void init() { + icon_left_arrows = gbitmap_create_with_resource(RESOURCE_ID_LEFT_ARROWS); + icon_right_arrows = gbitmap_create_with_resource(RESOURCE_ID_RIGHT_ARROWS); + icon_plus = gbitmap_create_with_resource(RESOURCE_ID_PLUS); + icon_minus = gbitmap_create_with_resource(RESOURCE_ID_MINUS); + icon_three_dots = gbitmap_create_with_resource(RESOURCE_ID_THREE_DOTS); + icon_pause = gbitmap_create_with_resource(RESOURCE_ID_PAUSE); + icon_play = gbitmap_create_with_resource(RESOURCE_ID_PLAY); + + app_window = create_metronome_window(); + + action_bar = action_bar_layer_create(); + action_bar_layer_add_to_window(action_bar, app_window); + primary_action_bar(); + + window_stack_push(app_window, true); +} + +static void deinit() { + window_destroy(app_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} diff --git a/src/c/metronome.c b/src/c/metronome.c new file mode 100644 index 0000000..f7a5bd3 --- /dev/null +++ b/src/c/metronome.c @@ -0,0 +1,205 @@ +/* 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); +} diff --git a/src/c/metronome.h b/src/c/metronome.h new file mode 100644 index 0000000..ee9e5f0 --- /dev/null +++ b/src/c/metronome.h @@ -0,0 +1,21 @@ +/* Copyright (C) 2026 Reiner Herrmann + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef METRONOME_H +#define METRONOME_H + +Window *create_metronome_window(); + +void beat_timer_callback(); + +void increase_bpm(); +void decrease_bpm(); +void increase_beats(); +void decrease_beats(); + +bool is_paused(); +void start_metronome(); +void stop_metronome(); + +#endif // METRONOME_H -- cgit v1.2.3