summaryrefslogtreecommitdiff
path: root/src/c/metronome.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/metronome.c
initial committrunk
Diffstat (limited to 'src/c/metronome.c')
-rw-r--r--src/c/metronome.c205
1 files changed, 205 insertions, 0 deletions
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);
+}