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/metronome.c | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/c/metronome.c (limited to 'src/c/metronome.c') 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); +} -- cgit v1.2.3