From 431a5b1a12d89e353cf257a8c3727d5c97105015 Mon Sep 17 00:00:00 2001 From: Haseo Date: Thu, 13 Oct 2022 20:57:04 +0200 Subject: [PATCH] Music Beeper By Haseo Music Beeper By Haseo Thx to Majik --- applications/plugins/application.fam | 4 +- .../plugins/music_beeper/application.fam | 24 + .../plugins/music_beeper/music_beeper.c | 367 +++++++++++++ .../plugins/music_beeper/music_beeper_cli.c | 48 ++ .../music_beeper/music_beeper_worker.c | 506 ++++++++++++++++++ .../music_beeper/music_beeper_worker.h | 46 ++ 6 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 applications/plugins/music_beeper/application.fam create mode 100644 applications/plugins/music_beeper/music_beeper.c create mode 100644 applications/plugins/music_beeper/music_beeper_cli.c create mode 100644 applications/plugins/music_beeper/music_beeper_worker.c create mode 100644 applications/plugins/music_beeper/music_beeper_worker.h diff --git a/applications/plugins/application.fam b/applications/plugins/application.fam index 6d25e45aa..3e5a654aa 100644 --- a/applications/plugins/application.fam +++ b/applications/plugins/application.fam @@ -3,7 +3,9 @@ App( name="Basic applications for plug-in menu", apptype=FlipperAppType.METAPACKAGE, provides=[ - "music_player", + "music_player", + "music_beeper", "snake_game", + "bt_hid", ], ) diff --git a/applications/plugins/music_beeper/application.fam b/applications/plugins/music_beeper/application.fam new file mode 100644 index 000000000..4c74df5fd --- /dev/null +++ b/applications/plugins/music_beeper/application.fam @@ -0,0 +1,24 @@ +App( + appid="Music_Beeper", + name="Music Beeper", + apptype=FlipperAppType.EXTERNAL, + entry_point="music_beeper_app", + cdefines=["APP_MUSIC_BEEPER"], + requires=[ + "gui", + "dialogs", + ], + provides=["music_beeper_start"], + stack_size=2 * 1024, + order=45, + fap_icon="../../../assets/icons/Archive/music_10px.png", + fap_category="Music", +) + +App( + appid="music_beeper_start", + apptype=FlipperAppType.STARTUP, + entry_point="music_beeper_on_system_start", + requires=["music_beeper"], + order=30, +) diff --git a/applications/plugins/music_beeper/music_beeper.c b/applications/plugins/music_beeper/music_beeper.c new file mode 100644 index 000000000..a09cad226 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper.c @@ -0,0 +1,367 @@ +#include "music_beeper_worker.h" + +#include +#include + +#include +#include +#include +#include + +#define TAG "MusicBeeper" + +#define MUSIC_BEEPER_APP_PATH_FOLDER ANY_PATH("music_beeper") +#define MUSIC_BEEPER_APP_EXTENSION "*" + +#define MUSIC_BEEPER_SEMITONE_HISTORY_SIZE 4 + +typedef struct { + uint8_t semitone_history[MUSIC_BEEPER_SEMITONE_HISTORY_SIZE]; + uint8_t duration_history[MUSIC_BEEPER_SEMITONE_HISTORY_SIZE]; + + uint8_t volume; + uint8_t semitone; + uint8_t dots; + uint8_t duration; + float position; +} MusicBeeperModel; + +typedef struct { + MusicBeeperModel* model; + FuriMutex** model_mutex; + + FuriMessageQueue* input_queue; + + ViewPort* view_port; + Gui* gui; + + MusicBeeperWorker* worker; +} MusicBeeper; + +static const float MUSIC_BEEPER_VOLUMES[] = {0, .25, .5, .75, 1}; + +static const char* semitone_to_note(int8_t semitone) { + switch(semitone) { + case 0: + return "C"; + case 1: + return "C#"; + case 2: + return "D"; + case 3: + return "D#"; + case 4: + return "E"; + case 5: + return "F"; + case 6: + return "F#"; + case 7: + return "G"; + case 8: + return "G#"; + case 9: + return "A"; + case 10: + return "A#"; + case 11: + return "B"; + default: + return "--"; + } +} + +static bool is_white_note(uint8_t semitone, uint8_t id) { + switch(semitone) { + case 0: + if(id == 0) return true; + break; + case 2: + if(id == 1) return true; + break; + case 4: + if(id == 2) return true; + break; + case 5: + if(id == 3) return true; + break; + case 7: + if(id == 4) return true; + break; + case 9: + if(id == 5) return true; + break; + case 11: + if(id == 6) return true; + break; + default: + break; + } + + return false; +} + +static bool is_black_note(uint8_t semitone, uint8_t id) { + switch(semitone) { + case 1: + if(id == 0) return true; + break; + case 3: + if(id == 1) return true; + break; + case 6: + if(id == 3) return true; + break; + case 8: + if(id == 4) return true; + break; + case 10: + if(id == 5) return true; + break; + default: + break; + } + + return false; +} + +static void render_callback(Canvas* canvas, void* ctx) { + MusicBeeper* music_beeper = ctx; + furi_check(furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, 12, "MusicBeeper"); + + uint8_t x_pos = 0; + uint8_t y_pos = 24; + const uint8_t white_w = 10; + const uint8_t white_h = 40; + + const int8_t black_x = 6; + const int8_t black_y = -5; + const uint8_t black_w = 8; + const uint8_t black_h = 32; + + // white keys + for(size_t i = 0; i < 7; i++) { + if(is_white_note(music_beeper->model->semitone, i)) { + canvas_draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } else { + canvas_draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } + } + + // black keys + for(size_t i = 0; i < 7; i++) { + if(i != 2 && i != 6) { + canvas_set_color(canvas, ColorWhite); + canvas_draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + canvas_set_color(canvas, ColorBlack); + if(is_black_note(music_beeper->model->semitone, i)) { + canvas_draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } else { + canvas_draw_frame( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } + } + } + + // volume view_port + x_pos = 124; + y_pos = 0; + const uint8_t volume_h = + (64 / (COUNT_OF(MUSIC_BEEPER_VOLUMES) - 1)) * music_beeper->model->volume; + canvas_draw_frame(canvas, x_pos, y_pos, 4, 64); + canvas_draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h); + + // note stack view_port + x_pos = 73; + y_pos = 0; + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_frame(canvas, x_pos, y_pos, 49, 64); + canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64); + + char duration_text[16]; + for(uint8_t i = 0; i < MUSIC_BEEPER_SEMITONE_HISTORY_SIZE; i++) { + if(music_beeper->model->duration_history[i] == 0xFF) { + snprintf(duration_text, 15, "--"); + } else { + snprintf(duration_text, 15, "%d", music_beeper->model->duration_history[i]); + } + + if(i == 0) { + canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_set_color(canvas, ColorBlack); + } + canvas_draw_str( + canvas, + x_pos + 4, + 64 - 16 * i - 3, + semitone_to_note(music_beeper->model->semitone_history[i])); + canvas_draw_str(canvas, x_pos + 31, 64 - 16 * i - 3, duration_text); + canvas_draw_line(canvas, x_pos, 64 - 16 * i, x_pos + 48, 64 - 16 * i); + } + + furi_mutex_release(music_beeper->model_mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + MusicBeeper* music_beeper = ctx; + if(input_event->type == InputTypeShort) { + furi_message_queue_put(music_beeper->input_queue, input_event, 0); + } +} + +static void music_beeper_worker_callback( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context) { + MusicBeeper* music_beeper = context; + furi_check(furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + for(size_t i = 0; i < MUSIC_BEEPER_SEMITONE_HISTORY_SIZE - 1; i++) { + size_t r = MUSIC_BEEPER_SEMITONE_HISTORY_SIZE - 1 - i; + music_beeper->model->duration_history[r] = music_beeper->model->duration_history[r - 1]; + music_beeper->model->semitone_history[r] = music_beeper->model->semitone_history[r - 1]; + } + + semitone = (semitone == 0xFF) ? 0xFF : semitone % 12; + + music_beeper->model->semitone = semitone; + music_beeper->model->dots = dots; + music_beeper->model->duration = duration; + music_beeper->model->position = position; + + music_beeper->model->semitone_history[0] = semitone; + music_beeper->model->duration_history[0] = duration; + + furi_mutex_release(music_beeper->model_mutex); + view_port_update(music_beeper->view_port); +} + +void music_beeper_clear(MusicBeeper* instance) { + memset(instance->model->duration_history, 0xff, MUSIC_BEEPER_SEMITONE_HISTORY_SIZE); + memset(instance->model->semitone_history, 0xff, MUSIC_BEEPER_SEMITONE_HISTORY_SIZE); + music_beeper_worker_clear(instance->worker); +} + +MusicBeeper* music_beeper_alloc() { + MusicBeeper* instance = malloc(sizeof(MusicBeeper)); + + instance->model = malloc(sizeof(MusicBeeperModel)); + instance->model->volume = 4; + + instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + instance->worker = music_beeper_worker_alloc(); + music_beeper_worker_set_volume( + instance->worker, MUSIC_BEEPER_VOLUMES[instance->model->volume]); + music_beeper_worker_set_callback(instance->worker, music_beeper_worker_callback, instance); + + music_beeper_clear(instance); + + instance->view_port = view_port_alloc(); + view_port_draw_callback_set(instance->view_port, render_callback, instance); + view_port_input_callback_set(instance->view_port, input_callback, instance); + + // Open GUI and register view_port + instance->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + return instance; +} + +void music_beeper_free(MusicBeeper* instance) { + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close(RECORD_GUI); + view_port_free(instance->view_port); + + music_beeper_worker_free(instance->worker); + + furi_message_queue_free(instance->input_queue); + + furi_mutex_free(instance->model_mutex); + + free(instance->model); + free(instance); +} + +int32_t music_beeper_app(void* p) { + MusicBeeper* music_beeper = music_beeper_alloc(); + + FuriString* file_path; + file_path = furi_string_alloc(); + + do { + if(p && strlen(p)) { + furi_string_set(file_path, (const char*)p); + } else { + furi_string_set(file_path, MUSIC_BEEPER_APP_PATH_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, MUSIC_BEEPER_APP_EXTENSION, &I_music_10px); + browser_options.hide_ext = false; + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options); + + furi_record_close(RECORD_DIALOGS); + if(!res) { + FURI_LOG_E(TAG, "No file selected"); + break; + } + } + + if(!music_beeper_worker_load(music_beeper->worker, furi_string_get_cstr(file_path))) { + FURI_LOG_E(TAG, "Unable to load file"); + break; + } + + music_beeper_worker_start(music_beeper->worker); + + InputEvent input; + while(furi_message_queue_get(music_beeper->input_queue, &input, FuriWaitForever) == + FuriStatusOk) { + furi_check( + furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + if(input.key == InputKeyBack) { + furi_mutex_release(music_beeper->model_mutex); + break; + } else if(input.key == InputKeyUp) { + if(music_beeper->model->volume < COUNT_OF(MUSIC_BEEPER_VOLUMES) - 1) + music_beeper->model->volume++; + music_beeper_worker_set_volume( + music_beeper->worker, MUSIC_BEEPER_VOLUMES[music_beeper->model->volume]); + } else if(input.key == InputKeyDown) { + if(music_beeper->model->volume > 0) music_beeper->model->volume--; + music_beeper_worker_set_volume( + music_beeper->worker, MUSIC_BEEPER_VOLUMES[music_beeper->model->volume]); + } + + furi_mutex_release(music_beeper->model_mutex); + view_port_update(music_beeper->view_port); + } + + music_beeper_worker_stop(music_beeper->worker); + if(p && strlen(p)) break; // Exit instead of going to browser if launched with arg + music_beeper_clear(music_beeper); + } while(1); + + furi_string_free(file_path); + music_beeper_free(music_beeper); + + return 0; +} diff --git a/applications/plugins/music_beeper/music_beeper_cli.c b/applications/plugins/music_beeper/music_beeper_cli.c new file mode 100644 index 000000000..26299fa64 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_cli.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include "music_beeper_worker.h" + +static void music_beeper_cli(Cli* cli, FuriString* args, void* context) { + UNUSED(context); + MusicBeeperWorker* music_beeper_worker = music_beeper_worker_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + + do { + if(storage_common_stat(storage, furi_string_get_cstr(args), NULL) == FSE_OK) { + if(!music_beeper_worker_load(music_beeper_worker, furi_string_get_cstr(args))) { + printf("Failed to open file %s\r\n", furi_string_get_cstr(args)); + break; + } + } else { + if(!music_beeper_worker_load_rtttl_from_string( + music_beeper_worker, furi_string_get_cstr(args))) { + printf("Argument is not a file or RTTTL\r\n"); + break; + } + } + + printf("Press CTRL+C to stop\r\n"); + music_beeper_worker_set_volume(music_beeper_worker, 1.0f); + music_beeper_worker_start(music_beeper_worker); + while(!cli_cmd_interrupt_received(cli)) { + furi_delay_ms(50); + } + music_beeper_worker_stop(music_beeper_worker); + } while(0); + + furi_record_close(RECORD_STORAGE); + music_beeper_worker_free(music_beeper_worker); +} + +void music_beeper_on_system_start() { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + + cli_add_command(cli, "music_beeper", CliCommandFlagDefault, music_beeper_cli, NULL); + + furi_record_close(RECORD_CLI); +#else + UNUSED(music_beeper_cli); +#endif +} diff --git a/applications/plugins/music_beeper/music_beeper_worker.c b/applications/plugins/music_beeper/music_beeper_worker.c new file mode 100644 index 000000000..711ac1b2e --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_worker.c @@ -0,0 +1,506 @@ +#include "music_beeper_worker.h" + +#include +#include + +#include +#include + +#include + +#define TAG "MusicBeeperWorker" + +#define MUSIC_BEEPER_FILETYPE "Flipper Music Format" +#define MUSIC_BEEPER_VERSION 0 + +#define SEMITONE_PAUSE 0xFF + +#define NOTE_C4 261.63f +#define NOTE_C4_SEMITONE (4.0f * 12.0f) +#define TWO_POW_TWELTH_ROOT 1.059463094359f + +typedef struct { + uint8_t semitone; + uint8_t duration; + uint8_t dots; +} NoteBlock; + +ARRAY_DEF(NoteBlockArray, NoteBlock, M_POD_OPLIST); + +struct MusicBeeperWorker { + FuriThread* thread; + bool should_work; + + MusicBeeperWorkerCallback callback; + void* callback_context; + + float volume; + uint32_t bpm; + uint32_t duration; + uint32_t octave; + NoteBlockArray_t notes; +}; + +static int32_t music_beeper_worker_thread_callback(void* context) { + furi_assert(context); + MusicBeeperWorker* instance = context; + + NoteBlockArray_it_t it; + NoteBlockArray_it(it, instance->notes); + + while(instance->should_work) { + if(NoteBlockArray_end_p(it)) { + NoteBlockArray_it(it, instance->notes); + furi_delay_ms(10); + } else { + NoteBlock* note_block = NoteBlockArray_ref(it); + + float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE; + float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4); + float duration = + 60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm / note_block->duration; + uint32_t dots = note_block->dots; + while(dots > 0) { + duration += duration / 2; + dots--; + } + uint32_t next_tick = furi_get_tick() + duration; + float volume = instance->volume; + + if(instance->callback) { + instance->callback( + note_block->semitone, + note_block->dots, + note_block->duration, + 0.0, + instance->callback_context); + } + + furi_hal_speaker_stop(); + furi_hal_speaker_start(frequency, volume); + while(instance->should_work && furi_get_tick() < next_tick) { + volume *= 1; + furi_hal_speaker_set_volume(volume); + furi_delay_ms(2); + } + NoteBlockArray_next(it); + } + } + + furi_hal_speaker_stop(); + + return 0; +} + +MusicBeeperWorker* music_beeper_worker_alloc() { + MusicBeeperWorker* instance = malloc(sizeof(MusicBeeperWorker)); + + NoteBlockArray_init(instance->notes); + + instance->thread = furi_thread_alloc(); + furi_thread_set_name(instance->thread, "MusicBeeperWorker"); + furi_thread_set_stack_size(instance->thread, 1024); + furi_thread_set_context(instance->thread, instance); + furi_thread_set_callback(instance->thread, music_beeper_worker_thread_callback); + + instance->volume = 1.0f; + + return instance; +} + +void music_beeper_worker_clear(MusicBeeperWorker* instance) { + NoteBlockArray_reset(instance->notes); +} + +void music_beeper_worker_free(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_thread_free(instance->thread); + NoteBlockArray_clear(instance->notes); + free(instance); +} + +static bool is_digit(const char c) { + return isdigit(c) != 0; +} + +static bool is_letter(const char c) { + return islower(c) != 0 || isupper(c) != 0; +} + +static bool is_space(const char c) { + return c == ' ' || c == '\t'; +} + +static size_t extract_number(const char* string, uint32_t* number) { + size_t ret = 0; + *number = 0; + while(is_digit(*string)) { + *number *= 10; + *number += (*string - '0'); + string++; + ret++; + } + return ret; +} + +static size_t extract_dots(const char* string, uint32_t* number) { + size_t ret = 0; + *number = 0; + while(*string == '.') { + *number += 1; + string++; + ret++; + } + return ret; +} + +static size_t extract_char(const char* string, char* symbol) { + if(is_letter(*string)) { + *symbol = *string; + return 1; + } else { + return 0; + } +} + +static size_t extract_sharp(const char* string, char* symbol) { + if(*string == '#' || *string == '_') { + *symbol = '#'; + return 1; + } else { + return 0; + } +} + +static size_t skip_till(const char* string, const char symbol) { + size_t ret = 0; + while(*string != '\0' && *string != symbol) { + string++; + ret++; + } + if(*string != symbol) { + ret = 0; + } + return ret; +} + +static bool music_beeper_worker_add_note( + MusicBeeperWorker* instance, + uint8_t semitone, + uint8_t duration, + uint8_t dots) { + NoteBlock note_block; + + note_block.semitone = semitone; + note_block.duration = duration; + note_block.dots = dots; + + NoteBlockArray_push_back(instance->notes, note_block); + + return true; +} + +static int8_t note_to_semitone(const char note) { + switch(note) { + case 'C': + return 0; + // C# + case 'D': + return 2; + // D# + case 'E': + return 4; + case 'F': + return 5; + // F# + case 'G': + return 7; + // G# + case 'A': + return 9; + // A# + case 'B': + return 11; + default: + return 0; + } +} + +static bool music_beeper_worker_parse_notes(MusicBeeperWorker* instance, const char* string) { + const char* cursor = string; + bool result = true; + + while(*cursor != '\0') { + if(!is_space(*cursor)) { + uint32_t duration = 0; + char note_char = '\0'; + char sharp_char = '\0'; + uint32_t octave = 0; + uint32_t dots = 0; + + // Parsing + cursor += extract_number(cursor, &duration); + cursor += extract_char(cursor, ¬e_char); + cursor += extract_sharp(cursor, &sharp_char); + cursor += extract_number(cursor, &octave); + cursor += extract_dots(cursor, &dots); + + // Post processing + note_char = toupper(note_char); + if(!duration) { + duration = instance->duration; + } + if(!octave) { + octave = instance->octave; + } + + // Validation + bool is_valid = true; + is_valid &= (duration >= 1 && duration <= 128); + is_valid &= ((note_char >= 'A' && note_char <= 'G') || note_char == 'P'); + is_valid &= (sharp_char == '#' || sharp_char == '\0'); + is_valid &= (octave <= 16); + is_valid &= (dots <= 16); + if(!is_valid) { + FURI_LOG_E( + TAG, + "Invalid note: %lu%c%c%lu.%lu", + duration, + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots); + result = false; + break; + } + + // Note to semitones + uint8_t semitone = 0; + if(note_char == 'P') { + semitone = SEMITONE_PAUSE; + } else { + semitone += octave * 12; + semitone += note_to_semitone(note_char); + semitone += sharp_char == '#' ? 1 : 0; + } + + if(music_beeper_worker_add_note(instance, semitone, duration, dots)) { + FURI_LOG_D( + TAG, + "Added note: %c%c%lu.%lu = %u %lu", + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots, + semitone, + duration); + } else { + FURI_LOG_E( + TAG, + "Invalid note: %c%c%lu.%lu = %u %lu", + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots, + semitone, + duration); + } + cursor += skip_till(cursor, ','); + } + + if(*cursor != '\0') cursor++; + } + + return result; +} + +bool music_beeper_worker_load(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool ret = false; + if(strcasestr(file_path, ".fmf")) { + ret = music_beeper_worker_load_fmf_from_file(instance, file_path); + } else { + ret = music_beeper_worker_load_rtttl_from_file(instance, file_path); + } + return ret; +} + +bool music_beeper_worker_load_fmf_from_file(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + FuriString* temp_str; + temp_str = furi_string_alloc(); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* file = flipper_format_file_alloc(storage); + + do { + if(!flipper_format_file_open_existing(file, file_path)) break; + + uint32_t version = 0; + if(!flipper_format_read_header(file, temp_str, &version)) break; + if(furi_string_cmp_str(temp_str, MUSIC_BEEPER_FILETYPE) || + (version != MUSIC_BEEPER_VERSION)) { + FURI_LOG_E(TAG, "Incorrect file format or version"); + break; + } + + if(!flipper_format_read_uint32(file, "BPM", &instance->bpm, 1)) { + FURI_LOG_E(TAG, "BPM is missing"); + break; + } + if(!flipper_format_read_uint32(file, "Duration", &instance->duration, 1)) { + FURI_LOG_E(TAG, "Duration is missing"); + break; + } + if(!flipper_format_read_uint32(file, "Octave", &instance->octave, 1)) { + FURI_LOG_E(TAG, "Octave is missing"); + break; + } + + if(!flipper_format_read_string(file, "Notes", temp_str)) { + FURI_LOG_E(TAG, "Notes is missing"); + break; + } + + if(!music_beeper_worker_parse_notes(instance, furi_string_get_cstr(temp_str))) { + break; + } + + result = true; + } while(false); + + furi_record_close(RECORD_STORAGE); + flipper_format_free(file); + furi_string_free(temp_str); + + return result; +} + +bool music_beeper_worker_load_rtttl_from_file(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + FuriString* content; + content = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + do { + if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Unable to open file"); + break; + }; + + uint16_t ret = 0; + do { + uint8_t buffer[65] = {0}; + ret = storage_file_read(file, buffer, sizeof(buffer) - 1); + for(size_t i = 0; i < ret; i++) { + furi_string_push_back(content, buffer[i]); + } + } while(ret > 0); + + furi_string_trim(content); + if(!furi_string_size(content)) { + FURI_LOG_E(TAG, "Empty file"); + break; + } + + if(!music_beeper_worker_load_rtttl_from_string(instance, furi_string_get_cstr(content))) { + FURI_LOG_E(TAG, "Invalid file content"); + break; + } + + result = true; + } while(0); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + furi_string_free(content); + + return result; +} + +bool music_beeper_worker_load_rtttl_from_string(MusicBeeperWorker* instance, const char* string) { + furi_assert(instance); + + const char* cursor = string; + + // Skip name + cursor += skip_till(cursor, ':'); + if(*cursor != ':') { + return false; + } + + // Duration + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->duration); + + // Octave + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->octave); + + // BPM + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->bpm); + + // Notes + cursor += skip_till(cursor, ':'); + if(*cursor != ':') { + return false; + } + cursor++; + if(!music_beeper_worker_parse_notes(instance, cursor)) { + return false; + } + + return true; +} + +void music_beeper_worker_set_callback( + MusicBeeperWorker* instance, + MusicBeeperWorkerCallback callback, + void* context) { + furi_assert(instance); + instance->callback = callback; + instance->callback_context = context; +} + +void music_beeper_worker_set_volume(MusicBeeperWorker* instance, float volume) { + furi_assert(instance); + instance->volume = volume; +} + +void music_beeper_worker_start(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == false); + + instance->should_work = true; + furi_thread_start(instance->thread); +} + +void music_beeper_worker_stop(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == true); + + instance->should_work = false; + furi_thread_join(instance->thread); +} diff --git a/applications/plugins/music_beeper/music_beeper_worker.h b/applications/plugins/music_beeper/music_beeper_worker.h new file mode 100644 index 000000000..bc30abf81 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_worker.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*MusicBeeperWorkerCallback)( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context); + +typedef struct MusicBeeperWorker MusicBeeperWorker; + +MusicBeeperWorker* music_beeper_worker_alloc(); + +void music_beeper_worker_clear(MusicBeeperWorker* instance); + +void music_beeper_worker_free(MusicBeeperWorker* instance); + +bool music_beeper_worker_load(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_fmf_from_file(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_rtttl_from_file(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_rtttl_from_string(MusicBeeperWorker* instance, const char* string); + +void music_beeper_worker_set_callback( + MusicBeeperWorker* instance, + MusicBeeperWorkerCallback callback, + void* context); + +void music_beeper_worker_set_volume(MusicBeeperWorker* instance, float volume); + +void music_beeper_worker_start(MusicBeeperWorker* instance); + +void music_beeper_worker_stop(MusicBeeperWorker* instance); + +#ifdef __cplusplus +} +#endif