This repository has been archived by the owner on Jul 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d136006
Showing
3 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
App( | ||
appid="music_player", | ||
name="Music Player", | ||
apptype=FlipperAppType.EXTERNAL, | ||
entry_point="music_player_app", | ||
requires=[ | ||
"gui", | ||
"dialogs", | ||
], | ||
stack_size=2 * 1024, | ||
order=20, | ||
fap_icon="icons/music_10px.png", | ||
fap_category="Media", | ||
fap_icon_assets="icons", | ||
fap_libs=["music_worker"], | ||
) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
#include <music_worker/music_worker.h> | ||
|
||
#include <furi.h> | ||
#include <furi_hal.h> | ||
|
||
#include <music_player_icons.h> | ||
#include <gui/gui.h> | ||
#include <dialogs/dialogs.h> | ||
#include <storage/storage.h> | ||
|
||
#define TAG "MusicPlayer" | ||
|
||
#define MUSIC_PLAYER_APP_PATH_FOLDER ANY_PATH("music_player") | ||
#define MUSIC_PLAYER_APP_EXTENSION "*" | ||
|
||
#define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4 | ||
|
||
typedef struct { | ||
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 { | ||
MusicPlayerModel* model; | ||
FuriMutex** model_mutex; | ||
|
||
FuriMessageQueue* input_queue; | ||
|
||
ViewPort* view_port; | ||
Gui* gui; | ||
|
||
MusicWorker* worker; | ||
} MusicPlayer; | ||
|
||
static const float MUSIC_PLAYER_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) { | ||
MusicPlayer* music_player = ctx; | ||
furi_check(furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk); | ||
|
||
canvas_clear(canvas); | ||
canvas_set_color(canvas, ColorBlack); | ||
canvas_set_font(canvas, FontPrimary); | ||
canvas_draw_str(canvas, 0, 12, "MusicPlayer"); | ||
|
||
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_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); | ||
} | ||
} | ||
|
||
// 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_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 { | ||
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_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); | ||
|
||
// note stack view_port | ||
x_pos = 73; | ||
y_pos = 0; //-V1048 | ||
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_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, | ||
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); | ||
} | ||
|
||
furi_mutex_release(music_player->model_mutex); | ||
} | ||
|
||
static void input_callback(InputEvent* input_event, void* ctx) { | ||
MusicPlayer* music_player = ctx; | ||
if(input_event->type == InputTypeShort) { | ||
furi_message_queue_put(music_player->input_queue, input_event, 0); | ||
} | ||
} | ||
|
||
static void music_worker_callback( | ||
uint8_t semitone, | ||
uint8_t dots, | ||
uint8_t duration, | ||
float position, | ||
void* context) { | ||
MusicPlayer* music_player = context; | ||
furi_check(furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk); | ||
|
||
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]; | ||
} | ||
|
||
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; | ||
|
||
furi_mutex_release(music_player->model_mutex); | ||
view_port_update(music_player->view_port); | ||
} | ||
|
||
void music_player_clear(MusicPlayer* instance) { | ||
memset(instance->model->duration_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE); | ||
memset(instance->model->semitone_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE); | ||
music_worker_clear(instance->worker); | ||
} | ||
|
||
MusicPlayer* music_player_alloc() { | ||
MusicPlayer* instance = malloc(sizeof(MusicPlayer)); | ||
|
||
instance->model = malloc(sizeof(MusicPlayerModel)); | ||
instance->model->volume = 3; | ||
|
||
instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal); | ||
|
||
instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); | ||
|
||
instance->worker = music_worker_alloc(); | ||
music_worker_set_volume(instance->worker, MUSIC_PLAYER_VOLUMES[instance->model->volume]); | ||
music_worker_set_callback(instance->worker, music_worker_callback, instance); | ||
|
||
music_player_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_player_free(MusicPlayer* instance) { | ||
gui_remove_view_port(instance->gui, instance->view_port); | ||
furi_record_close(RECORD_GUI); | ||
view_port_free(instance->view_port); | ||
|
||
music_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_player_app(void* p) { | ||
MusicPlayer* music_player = music_player_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_PLAYER_APP_PATH_FOLDER); | ||
|
||
DialogsFileBrowserOptions browser_options; | ||
dialog_file_browser_set_basic_options( | ||
&browser_options, MUSIC_PLAYER_APP_EXTENSION, &I_music_10px); | ||
browser_options.hide_ext = false; | ||
browser_options.base_path = MUSIC_PLAYER_APP_PATH_FOLDER; | ||
|
||
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_worker_load(music_player->worker, furi_string_get_cstr(file_path))) { | ||
FURI_LOG_E(TAG, "Unable to load file"); | ||
break; | ||
} | ||
|
||
music_worker_start(music_player->worker); | ||
|
||
InputEvent input; | ||
while(furi_message_queue_get(music_player->input_queue, &input, FuriWaitForever) == | ||
FuriStatusOk) { | ||
furi_check( | ||
furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk); | ||
|
||
if(input.key == InputKeyBack) { | ||
furi_mutex_release(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_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_worker_set_volume( | ||
music_player->worker, MUSIC_PLAYER_VOLUMES[music_player->model->volume]); | ||
} | ||
|
||
furi_mutex_release(music_player->model_mutex); | ||
view_port_update(music_player->view_port); | ||
} | ||
|
||
music_worker_stop(music_player->worker); | ||
if(p && strlen(p)) break; // Exit instead of going to browser if launched with arg | ||
music_player_clear(music_player); | ||
} while(1); | ||
|
||
furi_string_free(file_path); | ||
music_player_free(music_player); | ||
|
||
return 0; | ||
} |