├── img ├── screenshot.png ├── wave_left_4x14.png └── wave_right_4x14.png ├── metronome_icon.png ├── icons └── ButtonUp_7x4.png ├── gui_extensions.h ├── application.fam ├── README.md ├── gui_extensions.c └── metronome.c /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panki27/Metronome/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /metronome_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panki27/Metronome/HEAD/metronome_icon.png -------------------------------------------------------------------------------- /icons/ButtonUp_7x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panki27/Metronome/HEAD/icons/ButtonUp_7x4.png -------------------------------------------------------------------------------- /img/wave_left_4x14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panki27/Metronome/HEAD/img/wave_left_4x14.png -------------------------------------------------------------------------------- /img/wave_right_4x14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panki27/Metronome/HEAD/img/wave_right_4x14.png -------------------------------------------------------------------------------- /gui_extensions.h: -------------------------------------------------------------------------------- 1 | void elements_button_top_right(Canvas* canvas, const char* str); 2 | 3 | void elements_button_top_left(Canvas* canvas, const char* str); 4 | -------------------------------------------------------------------------------- /application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="metronome", 3 | name="Metronome", 4 | apptype=FlipperAppType.PLUGIN, 5 | entry_point="metronome_app", 6 | cdefines=["APP_METRONOME"], 7 | requires=[ 8 | "gui", 9 | ], 10 | fap_icon="metronome_icon.png", 11 | fap_icon_assets="icons", 12 | fap_category="Music", 13 | stack_size=2 * 1024, 14 | order=20, 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metronome 2 | 3 | A metronome for the [Flipper Zero](https://flipperzero.one/) device. Goes along perfectly with my [BPM tapper](https://github.com/panki27/bpm-tapper). 4 | 5 | ![screenshot](img/screenshot.png) 6 | 7 | ## Features 8 | 9 | - BPM adjustable, fine and coarse (hold pressed) 10 | - Selectable amount of beats per bar 11 | - Selectable note length 12 | - First beat is pronounced 13 | - Progress indicator 14 | - LED flashes accordingly 15 | - 3 different settings: Beep, Vibrate, Silent (push Down to change) 16 | 17 | ## Compiling 18 | 19 | ``` 20 | ./fbt fap_metronome 21 | ``` 22 | -------------------------------------------------------------------------------- /gui_extensions.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "metronome_icons.h" 4 | 5 | //lib can only do bottom left/right 6 | void elements_button_top_left(Canvas* canvas, const char* str) { 7 | const uint8_t button_height = 12; 8 | const uint8_t vertical_offset = 3; 9 | const uint8_t horizontal_offset = 3; 10 | const uint8_t string_width = canvas_string_width(canvas, str); 11 | const Icon* icon = &I_ButtonUp_7x4; 12 | const uint8_t icon_h_offset = 3; 13 | const uint8_t icon_width_with_offset = icon->width + icon_h_offset; 14 | const uint8_t icon_v_offset = icon->height + vertical_offset; 15 | const uint8_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset; 16 | 17 | const uint8_t x = 0; 18 | const uint8_t y = 0 + button_height; 19 | 20 | canvas_draw_box(canvas, x, y - button_height, button_width, button_height); 21 | canvas_draw_line(canvas, x + button_width + 0, y - button_height, x + button_width + 0, y - 1); 22 | canvas_draw_line(canvas, x + button_width + 1, y - button_height, x + button_width + 1, y - 2); 23 | canvas_draw_line(canvas, x + button_width + 2, y - button_height, x + button_width + 2, y - 3); 24 | 25 | canvas_invert_color(canvas); 26 | canvas_draw_icon(canvas, x + horizontal_offset, y - icon_v_offset, &I_ButtonUp_7x4); 27 | canvas_draw_str( 28 | canvas, x + horizontal_offset + icon_width_with_offset, y - vertical_offset, str); 29 | canvas_invert_color(canvas); 30 | } 31 | 32 | void elements_button_top_right(Canvas* canvas, const char* str) { 33 | const uint8_t button_height = 12; 34 | const uint8_t vertical_offset = 3; 35 | const uint8_t horizontal_offset = 3; 36 | const uint8_t string_width = canvas_string_width(canvas, str); 37 | const Icon* icon = &I_ButtonUp_7x4; 38 | const uint8_t icon_h_offset = 3; 39 | const uint8_t icon_width_with_offset = icon->width + icon_h_offset; 40 | const uint8_t icon_v_offset = icon->height + vertical_offset; 41 | const uint8_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset; 42 | 43 | const uint8_t x = canvas_width(canvas); 44 | const uint8_t y = 0 + button_height; 45 | 46 | canvas_draw_box(canvas, x - button_width, y - button_height, button_width, button_height); 47 | canvas_draw_line(canvas, x - button_width - 1, y - button_height, x - button_width - 1, y - 1); 48 | canvas_draw_line(canvas, x - button_width - 2, y - button_height, x - button_width - 2, y - 2); 49 | canvas_draw_line(canvas, x - button_width - 3, y - button_height, x - button_width - 3, y - 3); 50 | 51 | canvas_invert_color(canvas); 52 | canvas_draw_str(canvas, x - button_width + horizontal_offset, y - vertical_offset, str); 53 | canvas_draw_icon( 54 | canvas, x - horizontal_offset - icon->width, y - icon_v_offset, &I_ButtonUp_7x4); 55 | canvas_invert_color(canvas); 56 | } 57 | -------------------------------------------------------------------------------- /metronome.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "gui_extensions.h" 15 | 16 | #define BPM_STEP_SIZE_FINE 0.5d 17 | #define BPM_STEP_SIZE_COARSE 10.0d 18 | #define BPM_BOUNDARY_LOW 10.0d 19 | #define BPM_BOUNDARY_HIGH 300.0d 20 | #define BEEP_DELAY_MS 50 21 | 22 | #define wave_bitmap_left_width 4 23 | #define wave_bitmap_left_height 14 24 | static uint8_t wave_bitmap_left_bits[] = { 25 | 0x08, 0x0C, 0x06, 0x06, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x06, 26 | 0x0C, 0x08 27 | }; 28 | 29 | #define wave_bitmap_right_width 4 30 | #define wave_bitmap_right_height 14 31 | static uint8_t wave_bitmap_right_bits[] = { 32 | 0x01, 0x03, 0x06, 0x06, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x06, 0x06, 33 | 0x03, 0x01 34 | }; 35 | 36 | typedef enum { 37 | EventTypeTick, 38 | EventTypeKey, 39 | } EventType; 40 | 41 | typedef struct { 42 | EventType type; 43 | InputEvent input; 44 | } PluginEvent; 45 | 46 | enum OutputMode { 47 | Loud, 48 | Vibro, 49 | Silent 50 | }; 51 | 52 | typedef struct { 53 | double bpm; 54 | bool playing; 55 | int beats_per_bar; 56 | int note_length; 57 | int current_beat; 58 | enum OutputMode output_mode; 59 | FuriTimer* timer; 60 | NotificationApp* notifications; 61 | } MetronomeState; 62 | 63 | static void render_callback(Canvas* const canvas, void* ctx) { 64 | const MetronomeState* metronome_state = acquire_mutex((ValueMutex*)ctx, 25); 65 | if(metronome_state == NULL) { 66 | return; 67 | } 68 | 69 | string_t tempStr; 70 | string_init(tempStr); 71 | 72 | canvas_draw_frame(canvas, 0, 0, 128, 64); 73 | 74 | canvas_set_font(canvas, FontPrimary); 75 | 76 | // draw bars/beat 77 | string_printf(tempStr, "%d/%d", metronome_state->beats_per_bar, metronome_state->note_length); 78 | canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, string_get_cstr(tempStr)); 79 | string_reset(tempStr); 80 | 81 | // draw BPM value 82 | string_printf(tempStr, "%.2f", metronome_state->bpm); 83 | canvas_set_font(canvas, FontBigNumbers); 84 | canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, string_get_cstr(tempStr)); 85 | string_reset(tempStr); 86 | 87 | // draw volume indicator 88 | // always draw first waves 89 | canvas_draw_xbm(canvas, 20, 17, wave_bitmap_left_width, wave_bitmap_left_height, wave_bitmap_left_bits); 90 | canvas_draw_xbm(canvas, canvas_width(canvas)-20-wave_bitmap_right_width, 17, wave_bitmap_right_width, wave_bitmap_right_height, wave_bitmap_right_bits); 91 | if (metronome_state->output_mode < Silent) { 92 | canvas_draw_xbm(canvas, 16, 17, wave_bitmap_left_width, wave_bitmap_left_height, wave_bitmap_left_bits); 93 | canvas_draw_xbm(canvas, canvas_width(canvas)-16-wave_bitmap_right_width, 17, wave_bitmap_right_width, wave_bitmap_right_height, wave_bitmap_right_bits); 94 | } 95 | if (metronome_state->output_mode < Vibro) { 96 | canvas_draw_xbm(canvas, 12, 17, wave_bitmap_left_width, wave_bitmap_left_height, wave_bitmap_left_bits); 97 | canvas_draw_xbm(canvas, canvas_width(canvas)-12-wave_bitmap_right_width, 17, wave_bitmap_right_width, wave_bitmap_right_height, wave_bitmap_right_bits); 98 | } 99 | // draw button prompts 100 | canvas_set_font(canvas, FontSecondary); 101 | elements_button_left(canvas, "Slow"); 102 | elements_button_right(canvas, "Fast"); 103 | if (metronome_state->playing) { 104 | elements_button_center(canvas, "Stop "); 105 | } else { 106 | elements_button_center(canvas, "Start"); 107 | } 108 | elements_button_top_left(canvas, "Push"); 109 | elements_button_top_right(canvas, "Hold"); 110 | 111 | // draw progress bar 112 | elements_progress_bar(canvas, 8, 36, 112, (float)metronome_state->current_beat/metronome_state->beats_per_bar); 113 | 114 | // cleanup 115 | string_clear(tempStr); 116 | release_mutex((ValueMutex*)ctx, metronome_state); 117 | } 118 | 119 | static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { 120 | furi_assert(event_queue); 121 | 122 | PluginEvent event = {.type = EventTypeKey, .input = *input_event}; 123 | furi_message_queue_put(event_queue, &event, FuriWaitForever); 124 | } 125 | 126 | static void timer_callback(void* ctx) { 127 | // this is where we go BEEP! 128 | MetronomeState* metronome_state = acquire_mutex((ValueMutex*)ctx, 25); 129 | metronome_state->current_beat++; 130 | if (metronome_state->current_beat > metronome_state->beats_per_bar) { 131 | metronome_state->current_beat = 1; 132 | } 133 | if (metronome_state->current_beat == 1) { 134 | // pronounced beat 135 | notification_message(metronome_state->notifications, &sequence_set_only_red_255); 136 | switch(metronome_state->output_mode) { 137 | case Loud: 138 | if (furi_hal_speaker_acquire(1000)) { 139 | furi_hal_speaker_start(440.0f, 1.0f); 140 | } 141 | break; 142 | case Vibro: 143 | notification_message(metronome_state->notifications, &sequence_set_vibro_on); 144 | break; 145 | case Silent: 146 | break; 147 | } 148 | } else { 149 | // unpronounced beat 150 | notification_message(metronome_state->notifications, &sequence_set_only_green_255); 151 | switch(metronome_state->output_mode) { 152 | case Loud: 153 | if (furi_hal_speaker_acquire(1000)) { 154 | furi_hal_speaker_start(220.0f, 1.0f); 155 | } 156 | break; 157 | case Vibro: 158 | notification_message(metronome_state->notifications, &sequence_set_vibro_on); 159 | break; 160 | case Silent: 161 | break; 162 | } 163 | }; 164 | 165 | // this is a bit of a kludge... if we are on vibro and unpronounced, stop vibro after half the usual duration 166 | switch(metronome_state->output_mode) { 167 | case Loud: 168 | furi_delay_ms(BEEP_DELAY_MS); 169 | if (furi_hal_speaker_is_mine()) { 170 | furi_hal_speaker_stop(); 171 | furi_hal_speaker_release(); 172 | } 173 | break; 174 | case Vibro: 175 | if (metronome_state->current_beat == 1) { 176 | furi_delay_ms(BEEP_DELAY_MS); 177 | notification_message(metronome_state->notifications, &sequence_reset_vibro); 178 | } else { 179 | furi_delay_ms((int)BEEP_DELAY_MS/2); 180 | notification_message(metronome_state->notifications, &sequence_reset_vibro); 181 | furi_delay_ms((int)BEEP_DELAY_MS/2); 182 | } 183 | break; 184 | case Silent: 185 | break; 186 | } 187 | notification_message(metronome_state->notifications, &sequence_reset_rgb); 188 | 189 | release_mutex((ValueMutex*)ctx, metronome_state); 190 | } 191 | 192 | static uint32_t state_to_sleep_ticks(MetronomeState* metronome_state) { 193 | // calculate time between beeps 194 | uint32_t tps = furi_kernel_get_tick_frequency(); 195 | double multiplier = 4.0d/metronome_state->note_length; 196 | double bps = (double)metronome_state->bpm / 60; 197 | return (uint32_t)(round(tps / bps) - ((BEEP_DELAY_MS/1000)*tps)) * multiplier; 198 | } 199 | 200 | static void update_timer(MetronomeState* metronome_state) { 201 | if (furi_timer_is_running(metronome_state->timer)) { 202 | furi_timer_stop(metronome_state->timer); 203 | furi_timer_start( 204 | metronome_state->timer, 205 | state_to_sleep_ticks(metronome_state) 206 | ); 207 | } 208 | } 209 | 210 | static void increase_bpm(MetronomeState* metronome_state, double amount) { 211 | metronome_state->bpm += amount; 212 | if(metronome_state->bpm > (double)BPM_BOUNDARY_HIGH) { 213 | metronome_state->bpm = BPM_BOUNDARY_HIGH; 214 | } 215 | update_timer(metronome_state); 216 | } 217 | 218 | static void decrease_bpm(MetronomeState* metronome_state, double amount) { 219 | metronome_state->bpm -= amount; 220 | if(metronome_state->bpm < (double)BPM_BOUNDARY_LOW) { 221 | metronome_state->bpm = BPM_BOUNDARY_LOW; 222 | } 223 | update_timer(metronome_state); 224 | } 225 | 226 | static void cycle_beats_per_bar(MetronomeState* metronome_state) { 227 | metronome_state->beats_per_bar++; 228 | if (metronome_state->beats_per_bar > metronome_state->note_length) { 229 | metronome_state->beats_per_bar = 1; 230 | } 231 | } 232 | 233 | static void cycle_note_length(MetronomeState* metronome_state) { 234 | metronome_state->note_length *= 2; 235 | if (metronome_state->note_length > 16) { 236 | metronome_state->note_length = 2; 237 | metronome_state->beats_per_bar = 1; 238 | } 239 | update_timer(metronome_state); 240 | } 241 | 242 | static void cycle_output_mode(MetronomeState* metronome_state) { 243 | metronome_state->output_mode++; 244 | if (metronome_state->output_mode > Silent) { 245 | metronome_state->output_mode = Loud; 246 | } 247 | } 248 | 249 | static void metronome_state_init(MetronomeState* const metronome_state) { 250 | metronome_state->bpm = 120.0; 251 | metronome_state->playing = false; 252 | metronome_state->beats_per_bar = 4; 253 | metronome_state->note_length = 4; 254 | metronome_state->current_beat = 0; 255 | metronome_state->output_mode = Loud; 256 | metronome_state->notifications = furi_record_open(RECORD_NOTIFICATION); 257 | } 258 | 259 | int32_t metronome_app() { 260 | FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); 261 | 262 | MetronomeState* metronome_state = malloc(sizeof(MetronomeState)); 263 | metronome_state_init(metronome_state); 264 | 265 | ValueMutex state_mutex; 266 | if(!init_mutex(&state_mutex, metronome_state, sizeof(MetronomeState))) { 267 | FURI_LOG_E("Metronome", "cannot create mutex\r\n"); 268 | free(metronome_state); 269 | return 255; 270 | } 271 | 272 | // Set system callbacks 273 | ViewPort* view_port = view_port_alloc(); 274 | view_port_draw_callback_set(view_port, render_callback, &state_mutex); 275 | view_port_input_callback_set(view_port, input_callback, event_queue); 276 | metronome_state->timer = furi_timer_alloc(timer_callback, FuriTimerTypePeriodic, &state_mutex); 277 | 278 | // Open GUI and register view_port 279 | // 280 | Gui* gui = furi_record_open("gui"); 281 | gui_add_view_port(gui, view_port, GuiLayerFullscreen); 282 | 283 | PluginEvent event; 284 | for(bool processing = true; processing;) { 285 | FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100); 286 | 287 | MetronomeState* metronome_state = (MetronomeState*)acquire_mutex_block(&state_mutex); 288 | 289 | if(event_status == FuriStatusOk) { 290 | if(event.type == EventTypeKey) { 291 | if(event.input.type == InputTypeShort) { 292 | // push events 293 | switch(event.input.key) { 294 | case InputKeyUp: 295 | cycle_beats_per_bar(metronome_state); 296 | break; 297 | case InputKeyDown: 298 | cycle_output_mode(metronome_state); 299 | break; 300 | case InputKeyRight: 301 | increase_bpm(metronome_state, BPM_STEP_SIZE_FINE); 302 | break; 303 | case InputKeyLeft: 304 | decrease_bpm(metronome_state, BPM_STEP_SIZE_FINE); 305 | break; 306 | case InputKeyOk: 307 | metronome_state->playing = !metronome_state->playing; 308 | if (metronome_state->playing) { 309 | furi_timer_start(metronome_state->timer, state_to_sleep_ticks(metronome_state)); 310 | } else { 311 | furi_timer_stop(metronome_state->timer); 312 | } 313 | break; 314 | case InputKeyBack: 315 | processing = false; 316 | break; 317 | case InputKeyMAX: 318 | break; 319 | } 320 | } else if (event.input.type == InputTypeLong) { 321 | // hold events 322 | switch(event.input.key) { 323 | case InputKeyUp: 324 | cycle_note_length(metronome_state); 325 | break; 326 | case InputKeyDown: 327 | break; 328 | case InputKeyRight: 329 | increase_bpm(metronome_state, BPM_STEP_SIZE_COARSE); 330 | break; 331 | case InputKeyLeft: 332 | decrease_bpm(metronome_state, BPM_STEP_SIZE_COARSE); 333 | break; 334 | case InputKeyOk: 335 | break; 336 | case InputKeyBack: 337 | processing = false; 338 | break; 339 | case InputKeyMAX: 340 | break; 341 | } 342 | } else if (event.input.type == InputTypeRepeat) { 343 | // repeat events 344 | switch(event.input.key) { 345 | case InputKeyUp: 346 | break; 347 | case InputKeyDown: 348 | break; 349 | case InputKeyRight: 350 | increase_bpm(metronome_state, BPM_STEP_SIZE_COARSE); 351 | break; 352 | case InputKeyLeft: 353 | decrease_bpm(metronome_state, BPM_STEP_SIZE_COARSE); 354 | break; 355 | case InputKeyOk: 356 | break; 357 | case InputKeyBack: 358 | processing = false; 359 | break; 360 | case InputKeyMAX: 361 | break; 362 | } 363 | } 364 | } 365 | } else { 366 | FURI_LOG_D("Metronome", "FuriMessageQueue: event timeout"); 367 | // event timeout 368 | } 369 | 370 | view_port_update(view_port); 371 | release_mutex(&state_mutex, metronome_state); 372 | } 373 | 374 | view_port_enabled_set(view_port, false); 375 | gui_remove_view_port(gui, view_port); 376 | furi_record_close("gui"); 377 | view_port_free(view_port); 378 | furi_message_queue_free(event_queue); 379 | delete_mutex(&state_mutex); 380 | furi_timer_free(metronome_state->timer); 381 | furi_record_close(RECORD_NOTIFICATION); 382 | free(metronome_state); 383 | 384 | return 0; 385 | } 386 | --------------------------------------------------------------------------------