summaryrefslogtreecommitdiff
path: root/src/c
diff options
context:
space:
mode:
authorReiner Herrmann <reiner@reiner-h.de>2026-07-08 00:23:22 +0200
committerReiner Herrmann <reiner@reiner-h.de>2026-07-08 00:23:22 +0200
commitdf2fbf4c9291cce340945a0a71de0200ccae7369 (patch)
tree86573b08e2e8beb8e7bffbb2af1394ecd81ae7d4 /src/c
initial committrunk
Diffstat (limited to 'src/c')
-rw-r--r--src/c/app.c114
-rw-r--r--src/c/metronome.c205
-rw-r--r--src/c/metronome.h21
3 files changed, 340 insertions, 0 deletions
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 <pebble.h>
+#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 <pebble.h>
+
+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(&note, 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; i++) {
+ GColor color1, color2;
+ if (current_beat % (2 * beats) >= 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