diff --git a/applications/applications.c b/applications/applications.c index b9e8bbd4..4deae885 100644 --- a/applications/applications.c +++ b/applications/applications.c @@ -55,6 +55,7 @@ extern void crypto_on_system_start(); extern void ibutton_on_system_start(); extern void infrared_on_system_start(); extern void lfrfid_on_system_start(); +extern void music_player_on_system_start(); extern void nfc_on_system_start(); extern void storage_on_system_start(); extern void subghz_on_system_start(); @@ -280,6 +281,10 @@ const FlipperOnStartHook FLIPPER_ON_SYSTEM_START[] = { infrared_on_system_start, #endif +#ifdef APP_MUSIC_PLAYER + music_player_on_system_start, +#endif + #ifdef APP_NFC nfc_on_system_start, #endif @@ -332,7 +337,7 @@ const FlipperApplication FLIPPER_PLUGINS[] = { #ifdef APP_MUSIC_PLAYER {.app = music_player_app, .name = "Music Player", - .stack_size = 1024, + .stack_size = 2048, .icon = &A_Plugins_14, .flags = FlipperApplicationFlagDefault}, #endif diff --git a/applications/music_player/music_player.c b/applications/music_player/music_player.c index 4f8813d6..73f4bd2c 100644 --- a/applications/music_player/music_player.c +++ b/applications/music_player/music_player.c @@ -2,132 +2,93 @@ #include #include -#include +#include +#include "music_player_worker.h" -// TODO float note freq -typedef enum { - // Delay - N = 0, - // Octave 4 - B4 = 494, - // Octave 5 - C5 = 523, - D5 = 587, - E5 = 659, - F_5 = 740, - G5 = 784, - A5 = 880, - B5 = 988, - // Octave 6 - C6 = 1046, - D6 = 1175, - E6 = 1319, -} MelodyEventNote; +#define TAG "MusicPlayer" -typedef enum { - L1 = 1, - L2 = 2, - L4 = 4, - L8 = 8, - L16 = 16, - L32 = 32, - L64 = 64, - L128 = 128, -} MelodyEventLength; +#define MUSIC_PLAYER_APP_PATH_FOLDER "/any/music_player" +#define MUSIC_PLAYER_APP_EXTENSION "*" + +#define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4 typedef struct { - MelodyEventNote note; - MelodyEventLength length; -} MelodyEventRecord; + uint8_t semitone_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE]; + uint8_t duration_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE]; + + uint8_t volume; + uint8_t semitone; + uint8_t dots; + uint8_t duration; + float position; +} MusicPlayerModel; typedef struct { - const MelodyEventRecord* record; - int8_t loop_count; -} SongPattern; + MusicPlayerModel* model; + osMutexId_t* model_mutex; -const MelodyEventRecord melody_start[] = { - {E6, L8}, {N, L8}, {E5, L8}, {B5, L8}, {N, L4}, {E5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, - {E5, L8}, {B5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {D6, L8}, {N, L4}, {D5, L8}, {B5, L8}, - {N, L4}, {D5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {D5, L8}, {F_5, L8}, {N, L8}, {G5, L8}, - {A5, L8}, {D6, L8}, {N, L4}, {F_5, L8}, {B5, L8}, {N, L4}, {F_5, L8}, {D6, L8}, {C6, L8}, - {B5, L8}, {F_5, L8}, {A5, L8}, {N, L8}, {G5, L8}, {F_5, L8}, {E5, L8}, {N, L8}, {C5, L8}, - {E5, L8}, {B5, L8}, {B4, L8}, {C5, L8}, {D5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, {F_5, L8}, - {A5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {E6, L8}}; + osMessageQueueId_t input_queue; -const MelodyEventRecord melody_loop[] = { - {N, L4}, {E5, L8}, {B5, L8}, {N, L4}, {E5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {E5, L8}, - {B5, L8}, {N, L8}, {G5, L8}, {A5, L8}, {D6, L8}, {N, L4}, {D5, L8}, {B5, L8}, {N, L4}, - {D5, L8}, {A5, L8}, {G5, L8}, {A5, L8}, {D5, L8}, {F_5, L8}, {N, L8}, {G5, L8}, {A5, L8}, - {D6, L8}, {N, L4}, {F_5, L8}, {B5, L8}, {N, L4}, {F_5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, - {F_5, L8}, {A5, L8}, {N, L8}, {G5, L8}, {F_5, L8}, {E5, L8}, {N, L8}, {C5, L8}, {E5, L8}, - {B5, L8}, {B4, L8}, {C5, L8}, {D5, L8}, {D6, L8}, {C6, L8}, {B5, L8}, {F_5, L8}, {A5, L8}, - {N, L8}, {G5, L8}, {A5, L8}, {E6, L8}}; + ViewPort* view_port; + Gui* gui; -const MelodyEventRecord melody_chords_1bar[] = { - {E6, L8}, {N, L8}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, - {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, - {B4, L128}, {E5, L128}, {B5, L8}, {N, L4}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, - {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, - {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {A5, L8}}; + MusicPlayerWorker* worker; +} MusicPlayer; -const SongPattern song[] = {{melody_start, 1}, {melody_loop, -1}}; +static const float MUSIC_PLAYER_VOLUMES[] = {0, .25, .5, .75, 1}; -typedef enum { - EventTypeTick, - EventTypeKey, - EventTypeNote, - // add your events type -} MusicDemoEventType; +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 "--"; + } +} -typedef struct { - union { - InputEvent input; - const MelodyEventRecord* note_record; - } value; - MusicDemoEventType type; -} MusicDemoEvent; - -typedef struct { - ValueMutex* state_mutex; - osMessageQueueId_t event_queue; - -} MusicDemoContext; - -#define note_stack_size 4 -typedef struct { - // describe state here - const MelodyEventRecord* note_record; - const MelodyEventRecord* note_stack[note_stack_size]; - uint8_t volume_id; - uint8_t volume_id_max; -} State; - -const float volumes[] = {0, .25, .5, .75, 1}; - -bool is_white_note(const MelodyEventRecord* note_record, uint8_t id) { - if(note_record == NULL) return false; - - switch(note_record->note) { - case C5: - case C6: +static bool is_white_note(uint8_t semitone, uint8_t id) { + switch(semitone) { + case 0: if(id == 0) return true; break; - case D5: - case D6: + case 2: if(id == 1) return true; break; - case E5: - case E6: + case 4: if(id == 2) return true; break; - case G5: + case 5: + if(id == 3) return true; + break; + case 7: if(id == 4) return true; break; - case A5: + case 9: if(id == 5) return true; break; - case B4: - case B5: + case 11: if(id == 6) return true; break; default: @@ -137,13 +98,23 @@ bool is_white_note(const MelodyEventRecord* note_record, uint8_t id) { return false; } -bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) { - if(note_record == NULL) return false; - - switch(note_record->note) { - case F_5: +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; } @@ -151,87 +122,9 @@ bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) { return false; } -const char* get_note_name(const MelodyEventRecord* note_record) { - if(note_record == NULL) return ""; - - switch(note_record->note) { - case N: - return "---"; - break; - case B4: - return "B4-"; - break; - case C5: - return "C5-"; - break; - case D5: - return "D5-"; - break; - case E5: - return "E5-"; - break; - case F_5: - return "F#5"; - break; - case G5: - return "G5-"; - break; - case A5: - return "A5-"; - break; - case B5: - return "B5-"; - break; - case C6: - return "C6-"; - break; - case D6: - return "D6-"; - break; - case E6: - return "E6-"; - break; - default: - return "UNK"; - break; - } -} -const char* get_note_len_name(const MelodyEventRecord* note_record) { - if(note_record == NULL) return ""; - - switch(note_record->length) { - case L1: - return "1-"; - break; - case L2: - return "2-"; - break; - case L4: - return "4-"; - break; - case L8: - return "8-"; - break; - case L16: - return "16"; - break; - case L32: - return "32"; - break; - case L64: - return "64"; - break; - case L128: - return "1+"; - break; - default: - return "--"; - break; - } -} - static void render_callback(Canvas* canvas, void* ctx) { - State* state = (State*)acquire_mutex((ValueMutex*)ctx, 25); + MusicPlayer* music_player = ctx; + furi_check(osMutexAcquire(music_player->model_mutex, osWaitForever) == osOK); canvas_clear(canvas); canvas_set_color(canvas, ColorBlack); @@ -250,7 +143,7 @@ static void render_callback(Canvas* canvas, void* ctx) { // white keys for(size_t i = 0; i < 7; i++) { - if(is_white_note(state->note_record, i)) { + if(is_white_note(music_player->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); @@ -264,7 +157,7 @@ static void render_callback(Canvas* canvas, void* ctx) { 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(state->note_record, i)) { + if(is_black_note(music_player->model->semitone, i)) { canvas_draw_box( canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); } else { @@ -277,7 +170,8 @@ static void render_callback(Canvas* canvas, void* ctx) { // volume view_port x_pos = 124; y_pos = 0; - const uint8_t volume_h = (64 / (state->volume_id_max - 1)) * state->volume_id; + const uint8_t volume_h = + (64 / (COUNT_OF(MUSIC_PLAYER_VOLUMES) - 1)) * music_player->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); @@ -289,171 +183,175 @@ static void render_callback(Canvas* canvas, void* ctx) { canvas_draw_frame(canvas, x_pos, y_pos, 49, 64); canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64); - for(uint8_t i = 0; i < note_stack_size; i++) { + char duration_text[16]; + for(uint8_t i = 0; i < MUSIC_PLAYER_SEMITONE_HISTORY_SIZE; i++) { + if(music_player->model->duration_history[i] == 0xFF) { + snprintf(duration_text, 15, "--"); + } else { + snprintf(duration_text, 15, "%d", music_player->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, get_note_name(state->note_stack[i])); canvas_draw_str( - canvas, x_pos + 31, 64 - 16 * i - 3, get_note_len_name(state->note_stack[i])); + canvas, + x_pos + 4, + 64 - 16 * i - 3, + semitone_to_note(music_player->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); } - release_mutex((ValueMutex*)ctx, state); + osMutexRelease(music_player->model_mutex); } static void input_callback(InputEvent* input_event, void* ctx) { - osMessageQueueId_t event_queue = ctx; - - MusicDemoEvent event; - event.type = EventTypeKey; - event.value.input = *input_event; - osMessageQueuePut(event_queue, &event, 0, 0); + MusicPlayer* music_player = ctx; + if(input_event->type == InputTypeShort) { + osMessageQueuePut(music_player->input_queue, input_event, 0, 0); + } } -void process_note( - const MelodyEventRecord* note_record, - float bar_length_ms, - MusicDemoContext* context) { - MusicDemoEvent event; - // send note event - event.type = EventTypeNote; - event.value.note_record = note_record; - osMessageQueuePut(context->event_queue, &event, 0, 0); +static void music_player_worker_callback( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context) { + MusicPlayer* music_player = context; + furi_check(osMutexAcquire(music_player->model_mutex, osWaitForever) == osOK); - // read volume - State* state = (State*)acquire_mutex(context->state_mutex, 25); - float volume = volumes[state->volume_id]; - release_mutex(context->state_mutex, state); - - // play note - float note_delay = bar_length_ms / (float)note_record->length; - if(note_record->note != N) { - furi_hal_speaker_start(note_record->note, volume); + for(size_t i = 0; i < MUSIC_PLAYER_SEMITONE_HISTORY_SIZE - 1; i++) { + size_t r = MUSIC_PLAYER_SEMITONE_HISTORY_SIZE - 1 - i; + music_player->model->duration_history[r] = music_player->model->duration_history[r - 1]; + music_player->model->semitone_history[r] = music_player->model->semitone_history[r - 1]; } - furi_hal_delay_ms(note_delay); - furi_hal_speaker_stop(); + + semitone = (semitone == 0xFF) ? 0xFF : semitone % 12; + + music_player->model->semitone = semitone; + music_player->model->dots = dots; + music_player->model->duration = duration; + music_player->model->position = position; + + music_player->model->semitone_history[0] = semitone; + music_player->model->duration_history[0] = duration; + + osMutexRelease(music_player->model_mutex); + view_port_update(music_player->view_port); } -void music_player_thread(void* p) { - MusicDemoContext* context = (MusicDemoContext*)p; +MusicPlayer* music_player_alloc() { + MusicPlayer* instance = malloc(sizeof(MusicPlayer)); - const float bpm = 130.0f; - // 4/4 - const float bar_length_ms = (60.0f * 1000.0f / bpm) * 4; - const uint16_t melody_start_events_count = sizeof(melody_start) / sizeof(melody_start[0]); - const uint16_t melody_loop_events_count = sizeof(melody_loop) / sizeof(melody_loop[0]); + instance->model = malloc(sizeof(MusicPlayerModel)); + memset(instance->model->duration_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE); + memset(instance->model->semitone_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE); + instance->model->volume = 3; - for(size_t i = 0; i < melody_start_events_count; i++) { - process_note(&melody_start[i], bar_length_ms, context); - } + instance->model_mutex = osMutexNew(NULL); - while(1) { - for(size_t i = 0; i < melody_loop_events_count; i++) { - process_note(&melody_loop[i], bar_length_ms, context); - } - } + instance->input_queue = osMessageQueueNew(8, sizeof(InputEvent), NULL); + + instance->worker = music_player_worker_alloc(); + music_player_worker_set_volume( + instance->worker, MUSIC_PLAYER_VOLUMES[instance->model->volume]); + music_player_worker_set_callback(instance->worker, music_player_worker_callback, 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("gui"); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + return instance; +} + +void music_player_free(MusicPlayer* instance) { + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close("gui"); + view_port_free(instance->view_port); + + music_player_worker_free(instance->worker); + + osMessageQueueDelete(instance->input_queue); + + osMutexDelete(instance->model_mutex); + + free(instance->model); + free(instance); } int32_t music_player_app(void* p) { - osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(MusicDemoEvent), NULL); + MusicPlayer* music_player = music_player_alloc(); - State _state; - _state.note_record = NULL; - for(size_t i = 0; i < note_stack_size; i++) { - _state.note_stack[i] = NULL; - } - _state.volume_id = 1; - _state.volume_id_max = sizeof(volumes) / sizeof(volumes[0]); + string_t file_path; + string_init(file_path); - ValueMutex state_mutex; - if(!init_mutex(&state_mutex, &_state, sizeof(State))) { - printf("cannot create mutex\r\n"); - return 255; - } - - ViewPort* view_port = view_port_alloc(); - view_port_draw_callback_set(view_port, render_callback, &state_mutex); - view_port_input_callback_set(view_port, input_callback, event_queue); - - // Open GUI and register view_port - Gui* gui = furi_record_open("gui"); - gui_add_view_port(gui, view_port, GuiLayerFullscreen); - - // start player thread - // TODO change to fuirac_start - osThreadAttr_t player_attr = {.name = "music_player_thread", .stack_size = 512}; - MusicDemoContext context = {.state_mutex = &state_mutex, .event_queue = event_queue}; - osThreadId_t player = osThreadNew(music_player_thread, &context, &player_attr); - - if(player == NULL) { - printf("cannot create player thread\r\n"); - return 255; - } - - MusicDemoEvent event; - while(1) { - osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100); - - State* state = (State*)acquire_mutex_block(&state_mutex); - - if(event_status == osOK) { - if(event.type == EventTypeKey) { - // press events - if(event.value.input.type == InputTypeShort && - event.value.input.key == InputKeyBack) { - release_mutex(&state_mutex, state); - break; - } - - if(event.value.input.type == InputTypePress && - event.value.input.key == InputKeyUp) { - if(state->volume_id < state->volume_id_max - 1) state->volume_id++; - } - - if(event.value.input.type == InputTypePress && - event.value.input.key == InputKeyDown) { - if(state->volume_id > 0) state->volume_id--; - } - - if(event.value.input.type == InputTypePress && - event.value.input.key == InputKeyLeft) { - } - - if(event.value.input.type == InputTypePress && - event.value.input.key == InputKeyRight) { - } - - if(event.value.input.key == InputKeyOk) { - } - - } else if(event.type == EventTypeNote) { - state->note_record = event.value.note_record; - - for(size_t i = note_stack_size - 1; i > 0; i--) { - state->note_stack[i] = state->note_stack[i - 1]; - } - state->note_stack[0] = state->note_record; - } + do { + if(p) { + string_cat_str(file_path, p); } else { - // event timeout + char* file_name = malloc(256); + DialogsApp* dialogs = furi_record_open("dialogs"); + bool res = dialog_file_select_show( + dialogs, + MUSIC_PLAYER_APP_PATH_FOLDER, + MUSIC_PLAYER_APP_EXTENSION, + file_name, + 255, + NULL); + furi_record_close("dialogs"); + if(!res) { + FURI_LOG_E(TAG, "No file selected"); + break; + } + string_cat_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER); + string_cat_str(file_path, "/"); + string_cat_str(file_path, file_name); + free(file_name); } - view_port_update(view_port); - release_mutex(&state_mutex, state); - } + if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) { + FURI_LOG_E(TAG, "Unable to load file"); + break; + } - osThreadTerminate(player); - furi_hal_speaker_stop(); - view_port_enabled_set(view_port, false); - gui_remove_view_port(gui, view_port); - furi_record_close("gui"); - view_port_free(view_port); - osMessageQueueDelete(event_queue); - delete_mutex(&state_mutex); + music_player_worker_start(music_player->worker); + + InputEvent input; + while(osMessageQueueGet(music_player->input_queue, &input, NULL, osWaitForever) == osOK) { + furi_check(osMutexAcquire(music_player->model_mutex, osWaitForever) == osOK); + + if(input.key == InputKeyBack) { + osMutexRelease(music_player->model_mutex); + break; + } else if(input.key == InputKeyUp) { + if(music_player->model->volume < COUNT_OF(MUSIC_PLAYER_VOLUMES) - 1) + music_player->model->volume++; + music_player_worker_set_volume( + music_player->worker, MUSIC_PLAYER_VOLUMES[music_player->model->volume]); + } else if(input.key == InputKeyDown) { + if(music_player->model->volume > 0) music_player->model->volume--; + music_player_worker_set_volume( + music_player->worker, MUSIC_PLAYER_VOLUMES[music_player->model->volume]); + } + + osMutexRelease(music_player->model_mutex); + view_port_update(music_player->view_port); + } + + music_player_worker_stop(music_player->worker); + } while(0); + + string_clear(file_path); + music_player_free(music_player); return 0; } diff --git a/applications/music_player/music_player_cli.c b/applications/music_player/music_player_cli.c new file mode 100644 index 00000000..3c76cb84 --- /dev/null +++ b/applications/music_player/music_player_cli.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include "music_player_worker.h" + +static void music_player_cli(Cli* cli, string_t args, void* context) { + MusicPlayerWorker* music_player_worker = music_player_worker_alloc(); + Storage* storage = furi_record_open("storage"); + + do { + if(storage_common_stat(storage, string_get_cstr(args), NULL) == FSE_OK) { + if(!music_player_worker_load(music_player_worker, string_get_cstr(args))) { + printf("Failed to open file %s\r\n", string_get_cstr(args)); + break; + } + } else { + if(!music_player_worker_load_rtttl_from_string( + music_player_worker, string_get_cstr(args))) { + printf("Argument is not a file or RTTTL\r\n"); + break; + } + } + + printf("Press CTRL+C to stop\r\n"); + music_player_worker_start(music_player_worker); + while(!cli_cmd_interrupt_received(cli)) { + osDelay(50); + } + music_player_worker_stop(music_player_worker); + } while(0); + + furi_record_close("storage"); + music_player_worker_free(music_player_worker); +} + +void music_player_on_system_start() { +#ifdef SRV_CLI + Cli* cli = furi_record_open("cli"); + + cli_add_command(cli, "music_player", CliCommandFlagDefault, music_player_cli, NULL); + + furi_record_close("cli"); +#else + UNUSED(music_player_cli); +#endif +} diff --git a/applications/music_player/music_player_worker.c b/applications/music_player/music_player_worker.c new file mode 100644 index 00000000..2c80e6f5 --- /dev/null +++ b/applications/music_player/music_player_worker.c @@ -0,0 +1,496 @@ +#include "music_player_worker.h" + +#include +#include + +#include +#include + +#include + +#define TAG "MusicPlayerWorker" + +#define MUSIC_PLAYER_FILETYPE "Flipper Music Format" +#define MUSIC_PLAYER_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 MusicPlayerWorker { + FuriThread* thread; + bool should_work; + + MusicPlayerWorkerCallback callback; + void* callback_context; + + float volume; + uint32_t bpm; + uint32_t duration; + uint32_t octave; + NoteBlockArray_t notes; +}; + +static int32_t music_player_worker_thread_callback(void* context) { + furi_assert(context); + MusicPlayerWorker* 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); + osDelay(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 * osKernelGetTickFreq() * 4 / instance->bpm / note_block->duration; + while(note_block->dots > 0) { + duration += duration / 2; + note_block->dots--; + } + uint32_t next_tick = furi_hal_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_hal_get_tick() < next_tick) { + volume *= 0.9945679; + furi_hal_speaker_set_volume(volume); + furi_hal_delay_ms(2); + } + NoteBlockArray_next(it); + } + } + + furi_hal_speaker_stop(); + + return 0; +} + +MusicPlayerWorker* music_player_worker_alloc() { + MusicPlayerWorker* instance = malloc(sizeof(MusicPlayerWorker)); + + NoteBlockArray_init(instance->notes); + + instance->thread = furi_thread_alloc(); + furi_thread_set_name(instance->thread, "MusicPlayerWorker"); + furi_thread_set_stack_size(instance->thread, 1024); + furi_thread_set_context(instance->thread, instance); + furi_thread_set_callback(instance->thread, music_player_worker_thread_callback); + + return instance; +} + +void music_player_worker_free(MusicPlayerWorker* 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; + 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; + 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_player_worker_add_note( + MusicPlayerWorker* 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_player_worker_parse_notes(MusicPlayerWorker* 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 >= 0 && octave <= 16); + is_valid &= (dots >= 0 && dots <= 16); + if(!is_valid) { + FURI_LOG_E( + TAG, + "Invalid note: %u%c%c%u.%u", + 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_player_worker_add_note(instance, semitone, duration, dots)) { + FURI_LOG_D( + TAG, + "Added note: %c%c%u.%u = %u %u", + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots, + semitone, + duration); + } else { + FURI_LOG_E( + TAG, + "Invalid note: %c%c%u.%u = %u %u", + 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_player_worker_load(MusicPlayerWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool ret = false; + if(strcasestr(file_path, ".fmf")) { + ret = music_player_worker_load_fmf_from_file(instance, file_path); + } else { + ret = music_player_worker_load_rtttl_from_file(instance, file_path); + } + return ret; +} + +bool music_player_worker_load_fmf_from_file(MusicPlayerWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + string_t temp_str; + string_init(temp_str); + + Storage* storage = furi_record_open("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(string_cmp_str(temp_str, MUSIC_PLAYER_FILETYPE) || (version != MUSIC_PLAYER_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_player_worker_parse_notes(instance, string_get_cstr(temp_str))) { + break; + } + + result = true; + } while(false); + + furi_record_close("storage"); + flipper_format_free(file); + string_clear(temp_str); + + return result; +} + +bool music_player_worker_load_rtttl_from_file(MusicPlayerWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + string_t content; + string_init(content); + Storage* storage = furi_record_open("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++) { + string_push_back(content, buffer[i]); + } + } while(ret > 0); + + string_strim(content); + if(!string_size(content)) { + FURI_LOG_E(TAG, "Empty file"); + break; + } + + if(!music_player_worker_load_rtttl_from_string(instance, string_get_cstr(content))) { + FURI_LOG_E(TAG, "Invalid file content"); + break; + } + + result = true; + } while(0); + + storage_file_free(file); + furi_record_close("storage"); + string_clear(content); + + return result; +} + +bool music_player_worker_load_rtttl_from_string(MusicPlayerWorker* 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_player_worker_parse_notes(instance, cursor)) { + return false; + } + + return true; +} + +void music_player_worker_set_callback( + MusicPlayerWorker* instance, + MusicPlayerWorkerCallback callback, + void* context) { + furi_assert(instance); + instance->callback = callback; + instance->callback_context = context; +} + +void music_player_worker_set_volume(MusicPlayerWorker* instance, float volume) { + furi_assert(instance); + instance->volume = volume; +} + +void music_player_worker_start(MusicPlayerWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == false); + + instance->should_work = true; + furi_thread_start(instance->thread); +} + +void music_player_worker_stop(MusicPlayerWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == true); + + instance->should_work = false; + furi_thread_join(instance->thread); +} diff --git a/applications/music_player/music_player_worker.h b/applications/music_player/music_player_worker.h new file mode 100644 index 00000000..3aa99ea3 --- /dev/null +++ b/applications/music_player/music_player_worker.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +typedef void (*MusicPlayerWorkerCallback)( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context); + +typedef struct MusicPlayerWorker MusicPlayerWorker; + +MusicPlayerWorker* music_player_worker_alloc(); + +void music_player_worker_free(MusicPlayerWorker* instance); + +bool music_player_worker_load(MusicPlayerWorker* instance, const char* file_path); + +bool music_player_worker_load_fmf_from_file(MusicPlayerWorker* instance, const char* file_path); + +bool music_player_worker_load_rtttl_from_file(MusicPlayerWorker* instance, const char* file_path); + +bool music_player_worker_load_rtttl_from_string(MusicPlayerWorker* instance, const char* string); + +void music_player_worker_set_callback( + MusicPlayerWorker* instance, + MusicPlayerWorkerCallback callback, + void* context); + +void music_player_worker_set_volume(MusicPlayerWorker* instance, float volume); + +void music_player_worker_start(MusicPlayerWorker* instance); + +void music_player_worker_stop(MusicPlayerWorker* instance); diff --git a/assets/resources/Manifest b/assets/resources/Manifest index f19edd7d..08d95044 100644 --- a/assets/resources/Manifest +++ b/assets/resources/Manifest @@ -1,8 +1,9 @@ V:0 -T:1651076680 +T:1651524332 D:badusb D:dolphin D:infrared +D:music_player D:nfc D:subghz D:u2f @@ -223,6 +224,7 @@ F:f267f0654781049ca323b11bb4375519:581:dolphin/L3_Lab_research_128x54/frame_9.bm F:41106c0cbc5144f151b2b2d3daaa0527:727:dolphin/L3_Lab_research_128x54/meta.txt D:infrared/assets F:d895fda2f48c6cc4c55e8a398ff52e43:74300:infrared/assets/tv.ir +F:a157a80f5a668700403d870c23b9567d:470:music_player/Marble_Machine.fmf D:nfc/assets F:c6826a621d081d68309e4be424d3d974:4715:nfc/assets/aid.nfc F:86efbebdf41bb6bf15cc51ef88f069d5:2565:nfc/assets/country_code.nfc diff --git a/assets/resources/music_player/Marble_Machine.fmf b/assets/resources/music_player/Marble_Machine.fmf new file mode 100644 index 00000000..7403c9a0 --- /dev/null +++ b/assets/resources/music_player/Marble_Machine.fmf @@ -0,0 +1,6 @@ +Filetype: Flipper Music Format +Version: 0 +BPM: 130 +Duration: 8 +Octave: 5 +Notes: E6, P, E, B, 4P, E, A, G, A, E, B, P, G, A, D6, 4P, D, B, 4P, D, A, G, A, D, F#, P, G, A, D6, 4P, F#, B, 4P, F#, D6, C6, B, F#, A, P, G, F#, E, P, C, E, B, B4, C, D, D6, C6, B, F#, A, P, G, A, E6, 4P, E, B, 4P, E, A, G, A, E, B, P, G, A, D6, 4P, D, B, 4P, D, A, G, A, D, F#, P, G, A, D6, 4P, F#, B, 4P, F#, D6, C6, B, F#, A, P, G, F#, E, P, C, E, B, B4, C, D, D6, C6, B, F#, A, P, G, A, E6 diff --git a/firmware/targets/f7/furi_hal/furi_hal_resources.c b/firmware/targets/f7/furi_hal/furi_hal_resources.c index e548ffaf..4209e0cf 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_resources.c +++ b/firmware/targets/f7/furi_hal/furi_hal_resources.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -87,10 +88,19 @@ void furi_hal_resources_init_early() { SET_BIT(PWR->CR3, PWR_CR3_APC); // Hard reset USB + furi_hal_gpio_write(&gpio_usb_dm, 1); + furi_hal_gpio_write(&gpio_usb_dp, 1); furi_hal_gpio_init_simple(&gpio_usb_dm, GpioModeOutputOpenDrain); furi_hal_gpio_init_simple(&gpio_usb_dp, GpioModeOutputOpenDrain); furi_hal_gpio_write(&gpio_usb_dm, 0); furi_hal_gpio_write(&gpio_usb_dp, 0); + furi_hal_delay_us(5); // Device Driven disconnect: 2.5us + extra to compensate cables + furi_hal_gpio_write(&gpio_usb_dm, 1); + furi_hal_gpio_write(&gpio_usb_dp, 1); + furi_hal_gpio_init_simple(&gpio_usb_dm, GpioModeAnalog); + furi_hal_gpio_init_simple(&gpio_usb_dp, GpioModeAnalog); + furi_hal_gpio_write(&gpio_usb_dm, 0); + furi_hal_gpio_write(&gpio_usb_dp, 0); // External header pins furi_hal_gpio_init(&gpio_ext_pc0, GpioModeAnalog, GpioPullNo, GpioSpeedLow); diff --git a/firmware/targets/f7/furi_hal/furi_hal_speaker.c b/firmware/targets/f7/furi_hal/furi_hal_speaker.c index 00186198..03a7f094 100644 --- a/firmware/targets/f7/furi_hal/furi_hal_speaker.c +++ b/firmware/targets/f7/furi_hal/furi_hal_speaker.c @@ -20,15 +20,7 @@ void furi_hal_speaker_init() { &gpio_speaker, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16); } -void furi_hal_speaker_start(float frequency, float volume) { - if(volume == 0) { - return; - } - - if(volume < 0) volume = 0; - if(volume > 1) volume = 1; - volume = volume * volume * volume; - +static inline uint32_t furi_hal_speaker_calculate_autoreload(float frequency) { uint32_t autoreload = (SystemCoreClock / FURI_HAL_SPEAKER_PRESCALER / frequency) - 1; if(autoreload < 2) { autoreload = 2; @@ -36,35 +28,65 @@ void furi_hal_speaker_start(float frequency, float volume) { autoreload = UINT16_MAX; } - LL_TIM_InitTypeDef TIM_InitStruct = {0}; - TIM_InitStruct.Prescaler = FURI_HAL_SPEAKER_PRESCALER - 1; - TIM_InitStruct.Autoreload = autoreload; - LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct); + return autoreload; +} + +static inline uint32_t furi_hal_speaker_calculate_compare(float volume) { + if(volume < 0) volume = 0; + if(volume > 1) volume = 1; + volume = volume * volume * volume; #ifdef FURI_HAL_SPEAKER_NEW_VOLUME uint32_t compare_value = volume * FURI_HAL_SPEAKER_MAX_VOLUME; - uint32_t clip_value = volume * TIM_InitStruct.Autoreload / 2; + uint32_t clip_value = volume * LL_TIM_GetAutoReload(FURI_HAL_SPEAKER_TIMER) / 2; if(compare_value > clip_value) { compare_value = clip_value; } #else - uint32_t compare_value = volume * autoreload / 2; + uint32_t compare_value = volume * LL_TIM_GetAutoReload(FURI_HAL_SPEAKER_TIMER) / 2; #endif if(compare_value == 0) { compare_value = 1; } + return compare_value; +} + +void furi_hal_speaker_start(float frequency, float volume) { + if(volume <= 0) { + furi_hal_speaker_stop(); + return; + } + + LL_TIM_InitTypeDef TIM_InitStruct = {0}; + TIM_InitStruct.Prescaler = FURI_HAL_SPEAKER_PRESCALER - 1; + TIM_InitStruct.Autoreload = furi_hal_speaker_calculate_autoreload(frequency); + LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct); + LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0}; TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1; TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE; - TIM_OC_InitStruct.CompareValue = compare_value; + TIM_OC_InitStruct.CompareValue = furi_hal_speaker_calculate_compare(volume); LL_TIM_OC_Init(FURI_HAL_SPEAKER_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct); LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER); LL_TIM_EnableCounter(FURI_HAL_SPEAKER_TIMER); } +void furi_hal_speaker_set_volume(float volume) { + if(volume <= 0) { + furi_hal_speaker_stop(); + return; + } + +#if FURI_HAL_SPEAKER_CHANNEL == LL_TIM_CHANNEL_CH1 + LL_TIM_OC_SetCompareCH1(FURI_HAL_SPEAKER_TIMER, furi_hal_speaker_calculate_compare(volume)); +#else +#error Invalid channel +#endif +} + void furi_hal_speaker_stop() { LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER); LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER); diff --git a/firmware/targets/furi_hal_include/furi_hal_speaker.h b/firmware/targets/furi_hal_include/furi_hal_speaker.h index 35c89fb6..67de41d9 100644 --- a/firmware/targets/furi_hal_include/furi_hal_speaker.h +++ b/firmware/targets/furi_hal_include/furi_hal_speaker.h @@ -12,6 +12,8 @@ void furi_hal_speaker_init(); void furi_hal_speaker_start(float frequency, float volume); +void furi_hal_speaker_set_volume(float volume); + void furi_hal_speaker_stop(); #ifdef __cplusplus