Music player rework (#1189)
* Music player: cli tool and new worker * Music player cli: flush message * Music player: fix note calculation * MusicPlayer: fix # parsing and add magic * FuriHal: improve speaker volume handling. MusicPlayer: minor sustain improvements * MusicPlayer: fix buffer overseek * FuriHal: drop unused variables * MusicPlayer: LFO 4 magic * MusicPlayer: add RTTTL parser * MusicPlayer: refactoring and add file open dialog on start * MusicPlayer: fix memcpy issue and more * FuriHal: force disconnect USB on early init and then leave usb line alone for some time. * FuriHal: switch speaker to old volume. MusicPlayer: fix incorrect note history, and drop lfo from worker. Co-authored-by: DrZlo13 <who.just.the.doctor@gmail.com>
This commit is contained in:
		
							parent
							
								
									0c85b88873
								
							
						
					
					
						commit
						f5175e1388
					
				| @ -55,6 +55,7 @@ extern void crypto_on_system_start(); | |||||||
| extern void ibutton_on_system_start(); | extern void ibutton_on_system_start(); | ||||||
| extern void infrared_on_system_start(); | extern void infrared_on_system_start(); | ||||||
| extern void lfrfid_on_system_start(); | extern void lfrfid_on_system_start(); | ||||||
|  | extern void music_player_on_system_start(); | ||||||
| extern void nfc_on_system_start(); | extern void nfc_on_system_start(); | ||||||
| extern void storage_on_system_start(); | extern void storage_on_system_start(); | ||||||
| extern void subghz_on_system_start(); | extern void subghz_on_system_start(); | ||||||
| @ -280,6 +281,10 @@ const FlipperOnStartHook FLIPPER_ON_SYSTEM_START[] = { | |||||||
|     infrared_on_system_start, |     infrared_on_system_start, | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | #ifdef APP_MUSIC_PLAYER | ||||||
|  |     music_player_on_system_start, | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
| #ifdef APP_NFC | #ifdef APP_NFC | ||||||
|     nfc_on_system_start, |     nfc_on_system_start, | ||||||
| #endif | #endif | ||||||
| @ -332,7 +337,7 @@ const FlipperApplication FLIPPER_PLUGINS[] = { | |||||||
| #ifdef APP_MUSIC_PLAYER | #ifdef APP_MUSIC_PLAYER | ||||||
|     {.app = music_player_app, |     {.app = music_player_app, | ||||||
|      .name = "Music Player", |      .name = "Music Player", | ||||||
|      .stack_size = 1024, |      .stack_size = 2048, | ||||||
|      .icon = &A_Plugins_14, |      .icon = &A_Plugins_14, | ||||||
|      .flags = FlipperApplicationFlagDefault}, |      .flags = FlipperApplicationFlagDefault}, | ||||||
| #endif | #endif | ||||||
|  | |||||||
| @ -2,132 +2,93 @@ | |||||||
| #include <furi_hal.h> | #include <furi_hal.h> | ||||||
| 
 | 
 | ||||||
| #include <gui/gui.h> | #include <gui/gui.h> | ||||||
| #include <input/input.h> | #include <dialogs/dialogs.h> | ||||||
|  | #include "music_player_worker.h" | ||||||
| 
 | 
 | ||||||
| // TODO float note freq
 | #define TAG "MusicPlayer" | ||||||
| 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; |  | ||||||
| 
 | 
 | ||||||
| typedef enum { | #define MUSIC_PLAYER_APP_PATH_FOLDER "/any/music_player" | ||||||
|     L1 = 1, | #define MUSIC_PLAYER_APP_EXTENSION "*" | ||||||
|     L2 = 2, | 
 | ||||||
|     L4 = 4, | #define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4 | ||||||
|     L8 = 8, |  | ||||||
|     L16 = 16, |  | ||||||
|     L32 = 32, |  | ||||||
|     L64 = 64, |  | ||||||
|     L128 = 128, |  | ||||||
| } MelodyEventLength; |  | ||||||
| 
 | 
 | ||||||
| typedef struct { | typedef struct { | ||||||
|     MelodyEventNote note; |     uint8_t semitone_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE]; | ||||||
|     MelodyEventLength length; |     uint8_t duration_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE]; | ||||||
| } MelodyEventRecord; | 
 | ||||||
|  |     uint8_t volume; | ||||||
|  |     uint8_t semitone; | ||||||
|  |     uint8_t dots; | ||||||
|  |     uint8_t duration; | ||||||
|  |     float position; | ||||||
|  | } MusicPlayerModel; | ||||||
| 
 | 
 | ||||||
| typedef struct { | typedef struct { | ||||||
|     const MelodyEventRecord* record; |     MusicPlayerModel* model; | ||||||
|     int8_t loop_count; |     osMutexId_t* model_mutex; | ||||||
| } SongPattern; |  | ||||||
| 
 | 
 | ||||||
| const MelodyEventRecord melody_start[] = { |     osMessageQueueId_t input_queue; | ||||||
|     {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}}; |  | ||||||
| 
 | 
 | ||||||
| const MelodyEventRecord melody_loop[] = { |     ViewPort* view_port; | ||||||
|     {N, L4},   {E5, L8}, {B5, L8},  {N, L4},  {E5, L8},  {A5, L8},  {G5, L8}, {A5, L8},  {E5, L8}, |     Gui* gui; | ||||||
|     {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}}; |  | ||||||
| 
 | 
 | ||||||
| const MelodyEventRecord melody_chords_1bar[] = { |     MusicPlayerWorker* worker; | ||||||
|     {E6, L8},   {N, L8},    {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, {B4, L128}, {E5, L128}, | } MusicPlayer; | ||||||
|     {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}}; |  | ||||||
| 
 | 
 | ||||||
| const SongPattern song[] = {{melody_start, 1}, {melody_loop, -1}}; | static const float MUSIC_PLAYER_VOLUMES[] = {0, .25, .5, .75, 1}; | ||||||
| 
 | 
 | ||||||
| typedef enum { | static const char* semitone_to_note(int8_t semitone) { | ||||||
|     EventTypeTick, |     switch(semitone) { | ||||||
|     EventTypeKey, |     case 0: | ||||||
|     EventTypeNote, |         return "C"; | ||||||
|     // add your events type
 |     case 1: | ||||||
| } MusicDemoEventType; |         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 { | static bool is_white_note(uint8_t semitone, uint8_t id) { | ||||||
|     union { |     switch(semitone) { | ||||||
|         InputEvent input; |     case 0: | ||||||
|         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: |  | ||||||
|         if(id == 0) return true; |         if(id == 0) return true; | ||||||
|         break; |         break; | ||||||
|     case D5: |     case 2: | ||||||
|     case D6: |  | ||||||
|         if(id == 1) return true; |         if(id == 1) return true; | ||||||
|         break; |         break; | ||||||
|     case E5: |     case 4: | ||||||
|     case E6: |  | ||||||
|         if(id == 2) return true; |         if(id == 2) return true; | ||||||
|         break; |         break; | ||||||
|     case G5: |     case 5: | ||||||
|  |         if(id == 3) return true; | ||||||
|  |         break; | ||||||
|  |     case 7: | ||||||
|         if(id == 4) return true; |         if(id == 4) return true; | ||||||
|         break; |         break; | ||||||
|     case A5: |     case 9: | ||||||
|         if(id == 5) return true; |         if(id == 5) return true; | ||||||
|         break; |         break; | ||||||
|     case B4: |     case 11: | ||||||
|     case B5: |  | ||||||
|         if(id == 6) return true; |         if(id == 6) return true; | ||||||
|         break; |         break; | ||||||
|     default: |     default: | ||||||
| @ -137,13 +98,23 @@ bool is_white_note(const MelodyEventRecord* note_record, uint8_t id) { | |||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) { | static bool is_black_note(uint8_t semitone, uint8_t id) { | ||||||
|     if(note_record == NULL) return false; |     switch(semitone) { | ||||||
| 
 |     case 1: | ||||||
|     switch(note_record->note) { |         if(id == 0) return true; | ||||||
|     case F_5: |         break; | ||||||
|  |     case 3: | ||||||
|  |         if(id == 1) return true; | ||||||
|  |         break; | ||||||
|  |     case 6: | ||||||
|         if(id == 3) return true; |         if(id == 3) return true; | ||||||
|         break; |         break; | ||||||
|  |     case 8: | ||||||
|  |         if(id == 4) return true; | ||||||
|  |         break; | ||||||
|  |     case 10: | ||||||
|  |         if(id == 5) return true; | ||||||
|  |         break; | ||||||
|     default: |     default: | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
| @ -151,87 +122,9 @@ bool is_black_note(const MelodyEventRecord* note_record, uint8_t id) { | |||||||
|     return false; |     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) { | 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_clear(canvas); | ||||||
|     canvas_set_color(canvas, ColorBlack); |     canvas_set_color(canvas, ColorBlack); | ||||||
| @ -250,7 +143,7 @@ static void render_callback(Canvas* canvas, void* ctx) { | |||||||
| 
 | 
 | ||||||
|     // white keys
 |     // white keys
 | ||||||
|     for(size_t i = 0; i < 7; i++) { |     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); |             canvas_draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); | ||||||
|         } else { |         } else { | ||||||
|             canvas_draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); |             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_draw_box( | ||||||
|                 canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); |                 canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); | ||||||
|             canvas_set_color(canvas, ColorBlack); |             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_draw_box( | ||||||
|                     canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); |                     canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); | ||||||
|             } else { |             } else { | ||||||
| @ -277,7 +170,8 @@ static void render_callback(Canvas* canvas, void* ctx) { | |||||||
|     // volume view_port
 |     // volume view_port
 | ||||||
|     x_pos = 124; |     x_pos = 124; | ||||||
|     y_pos = 0; |     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_frame(canvas, x_pos, y_pos, 4, 64); | ||||||
|     canvas_draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h); |     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_frame(canvas, x_pos, y_pos, 49, 64); | ||||||
|     canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 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) { |         if(i == 0) { | ||||||
|             canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16); |             canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16); | ||||||
|             canvas_set_color(canvas, ColorWhite); |             canvas_set_color(canvas, ColorWhite); | ||||||
|         } else { |         } else { | ||||||
|             canvas_set_color(canvas, ColorBlack); |             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_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); |         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) { | static void input_callback(InputEvent* input_event, void* ctx) { | ||||||
|     osMessageQueueId_t event_queue = ctx; |     MusicPlayer* music_player = ctx; | ||||||
| 
 |     if(input_event->type == InputTypeShort) { | ||||||
|     MusicDemoEvent event; |         osMessageQueuePut(music_player->input_queue, input_event, 0, 0); | ||||||
|     event.type = EventTypeKey; |  | ||||||
|     event.value.input = *input_event; |  | ||||||
|     osMessageQueuePut(event_queue, &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); |  | ||||||
| 
 |  | ||||||
|     // 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); |  | ||||||
|     } |  | ||||||
|     furi_hal_delay_ms(note_delay); |  | ||||||
|     furi_hal_speaker_stop(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void music_player_thread(void* p) { |  | ||||||
|     MusicDemoContext* context = (MusicDemoContext*)p; |  | ||||||
| 
 |  | ||||||
|     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]); |  | ||||||
| 
 |  | ||||||
|     for(size_t i = 0; i < melody_start_events_count; i++) { |  | ||||||
|         process_note(&melody_start[i], bar_length_ms, context); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     while(1) { |  | ||||||
|         for(size_t i = 0; i < melody_loop_events_count; i++) { |  | ||||||
|             process_note(&melody_loop[i], bar_length_ms, context); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 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); | ||||||
|  | 
 | ||||||
|  |     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; | ||||||
|  | 
 | ||||||
|  |     osMutexRelease(music_player->model_mutex); | ||||||
|  |     view_port_update(music_player->view_port); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | MusicPlayer* music_player_alloc() { | ||||||
|  |     MusicPlayer* instance = malloc(sizeof(MusicPlayer)); | ||||||
|  | 
 | ||||||
|  |     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; | ||||||
|  | 
 | ||||||
|  |     instance->model_mutex = osMutexNew(NULL); | ||||||
|  | 
 | ||||||
|  |     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) { | int32_t music_player_app(void* p) { | ||||||
|     osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(MusicDemoEvent), NULL); |     MusicPlayer* music_player = music_player_alloc(); | ||||||
| 
 | 
 | ||||||
|     State _state; |     string_t file_path; | ||||||
|     _state.note_record = NULL; |     string_init(file_path); | ||||||
|     for(size_t i = 0; i < note_stack_size; i++) { | 
 | ||||||
|         _state.note_stack[i] = NULL; |     do { | ||||||
|  |         if(p) { | ||||||
|  |             string_cat_str(file_path, p); | ||||||
|  |         } else { | ||||||
|  |             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; | ||||||
|             } |             } | ||||||
|     _state.volume_id = 1; |             string_cat_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER); | ||||||
|     _state.volume_id_max = sizeof(volumes) / sizeof(volumes[0]); |             string_cat_str(file_path, "/"); | ||||||
| 
 |             string_cat_str(file_path, file_name); | ||||||
|     ValueMutex state_mutex; |             free(file_name); | ||||||
|     if(!init_mutex(&state_mutex, &_state, sizeof(State))) { |  | ||||||
|         printf("cannot create mutex\r\n"); |  | ||||||
|         return 255; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     ViewPort* view_port = view_port_alloc(); |         if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) { | ||||||
|     view_port_draw_callback_set(view_port, render_callback, &state_mutex); |             FURI_LOG_E(TAG, "Unable to load file"); | ||||||
|     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; |             break; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|                 if(event.value.input.type == InputTypePress && |         music_player_worker_start(music_player->worker); | ||||||
|                    event.value.input.key == InputKeyUp) { | 
 | ||||||
|                     if(state->volume_id < state->volume_id_max - 1) state->volume_id++; |         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]); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|                 if(event.value.input.type == InputTypePress && |             osMutexRelease(music_player->model_mutex); | ||||||
|                    event.value.input.key == InputKeyDown) { |             view_port_update(music_player->view_port); | ||||||
|                     if(state->volume_id > 0) state->volume_id--; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|                 if(event.value.input.type == InputTypePress && |         music_player_worker_stop(music_player->worker); | ||||||
|                    event.value.input.key == InputKeyLeft) { |     } while(0); | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 if(event.value.input.type == InputTypePress && |     string_clear(file_path); | ||||||
|                    event.value.input.key == InputKeyRight) { |     music_player_free(music_player); | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 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; |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             // event timeout
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         view_port_update(view_port); |  | ||||||
|         release_mutex(&state_mutex, state); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     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); |  | ||||||
| 
 | 
 | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								applications/music_player/music_player_cli.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								applications/music_player/music_player_cli.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | #include <furi.h> | ||||||
|  | #include <cli/cli.h> | ||||||
|  | #include <storage/storage.h> | ||||||
|  | #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 | ||||||
|  | } | ||||||
							
								
								
									
										496
									
								
								applications/music_player/music_player_worker.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										496
									
								
								applications/music_player/music_player_worker.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,496 @@ | |||||||
|  | #include "music_player_worker.h" | ||||||
|  | 
 | ||||||
|  | #include <furi_hal.h> | ||||||
|  | #include <furi.h> | ||||||
|  | 
 | ||||||
|  | #include <storage/storage.h> | ||||||
|  | #include <lib/flipper_format/flipper_format.h> | ||||||
|  | 
 | ||||||
|  | #include <m-array.h> | ||||||
|  | 
 | ||||||
|  | #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); | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								applications/music_player/music_player_worker.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								applications/music_player/music_player_worker.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <stdbool.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | 
 | ||||||
|  | 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); | ||||||
| @ -1,8 +1,9 @@ | |||||||
| V:0 | V:0 | ||||||
| T:1651076680 | T:1651524332 | ||||||
| D:badusb | D:badusb | ||||||
| D:dolphin | D:dolphin | ||||||
| D:infrared | D:infrared | ||||||
|  | D:music_player | ||||||
| D:nfc | D:nfc | ||||||
| D:subghz | D:subghz | ||||||
| D:u2f | 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 | F:41106c0cbc5144f151b2b2d3daaa0527:727:dolphin/L3_Lab_research_128x54/meta.txt | ||||||
| D:infrared/assets | D:infrared/assets | ||||||
| F:d895fda2f48c6cc4c55e8a398ff52e43:74300:infrared/assets/tv.ir | F:d895fda2f48c6cc4c55e8a398ff52e43:74300:infrared/assets/tv.ir | ||||||
|  | F:a157a80f5a668700403d870c23b9567d:470:music_player/Marble_Machine.fmf | ||||||
| D:nfc/assets | D:nfc/assets | ||||||
| F:c6826a621d081d68309e4be424d3d974:4715:nfc/assets/aid.nfc | F:c6826a621d081d68309e4be424d3d974:4715:nfc/assets/aid.nfc | ||||||
| F:86efbebdf41bb6bf15cc51ef88f069d5:2565:nfc/assets/country_code.nfc | F:86efbebdf41bb6bf15cc51ef88f069d5:2565:nfc/assets/country_code.nfc | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								assets/resources/music_player/Marble_Machine.fmf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								assets/resources/music_player/Marble_Machine.fmf
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
| @ -1,4 +1,5 @@ | |||||||
| #include <furi_hal_resources.h> | #include <furi_hal_resources.h> | ||||||
|  | #include <furi_hal_delay.h> | ||||||
| #include <furi.h> | #include <furi.h> | ||||||
| 
 | 
 | ||||||
| #include <stm32wbxx_ll_rcc.h> | #include <stm32wbxx_ll_rcc.h> | ||||||
| @ -87,10 +88,19 @@ void furi_hal_resources_init_early() { | |||||||
|     SET_BIT(PWR->CR3, PWR_CR3_APC); |     SET_BIT(PWR->CR3, PWR_CR3_APC); | ||||||
| 
 | 
 | ||||||
|     // Hard reset USB
 |     // 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_dm, GpioModeOutputOpenDrain); | ||||||
|     furi_hal_gpio_init_simple(&gpio_usb_dp, GpioModeOutputOpenDrain); |     furi_hal_gpio_init_simple(&gpio_usb_dp, GpioModeOutputOpenDrain); | ||||||
|     furi_hal_gpio_write(&gpio_usb_dm, 0); |     furi_hal_gpio_write(&gpio_usb_dm, 0); | ||||||
|     furi_hal_gpio_write(&gpio_usb_dp, 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
 |     // External header pins
 | ||||||
|     furi_hal_gpio_init(&gpio_ext_pc0, GpioModeAnalog, GpioPullNo, GpioSpeedLow); |     furi_hal_gpio_init(&gpio_ext_pc0, GpioModeAnalog, GpioPullNo, GpioSpeedLow); | ||||||
|  | |||||||
| @ -20,15 +20,7 @@ void furi_hal_speaker_init() { | |||||||
|         &gpio_speaker, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16); |         &gpio_speaker, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn14TIM16); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void furi_hal_speaker_start(float frequency, float volume) { | static inline uint32_t furi_hal_speaker_calculate_autoreload(float frequency) { | ||||||
|     if(volume == 0) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if(volume < 0) volume = 0; |  | ||||||
|     if(volume > 1) volume = 1; |  | ||||||
|     volume = volume * volume * volume; |  | ||||||
| 
 |  | ||||||
|     uint32_t autoreload = (SystemCoreClock / FURI_HAL_SPEAKER_PRESCALER / frequency) - 1; |     uint32_t autoreload = (SystemCoreClock / FURI_HAL_SPEAKER_PRESCALER / frequency) - 1; | ||||||
|     if(autoreload < 2) { |     if(autoreload < 2) { | ||||||
|         autoreload = 2; |         autoreload = 2; | ||||||
| @ -36,35 +28,65 @@ void furi_hal_speaker_start(float frequency, float volume) { | |||||||
|         autoreload = UINT16_MAX; |         autoreload = UINT16_MAX; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     LL_TIM_InitTypeDef TIM_InitStruct = {0}; |     return autoreload; | ||||||
|     TIM_InitStruct.Prescaler = FURI_HAL_SPEAKER_PRESCALER - 1; | } | ||||||
|     TIM_InitStruct.Autoreload = autoreload; | 
 | ||||||
|     LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct); | 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 | #ifdef FURI_HAL_SPEAKER_NEW_VOLUME | ||||||
|     uint32_t compare_value = volume * FURI_HAL_SPEAKER_MAX_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) { |     if(compare_value > clip_value) { | ||||||
|         compare_value = clip_value; |         compare_value = clip_value; | ||||||
|     } |     } | ||||||
| #else | #else | ||||||
|     uint32_t compare_value = volume * autoreload / 2; |     uint32_t compare_value = volume * LL_TIM_GetAutoReload(FURI_HAL_SPEAKER_TIMER) / 2; | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|     if(compare_value == 0) { |     if(compare_value == 0) { | ||||||
|         compare_value = 1; |         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}; |     LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0}; | ||||||
|     TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1; |     TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1; | ||||||
|     TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE; |     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_OC_Init(FURI_HAL_SPEAKER_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct); | ||||||
| 
 | 
 | ||||||
|     LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER); |     LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER); | ||||||
|     LL_TIM_EnableCounter(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() { | void furi_hal_speaker_stop() { | ||||||
|     LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER); |     LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER); | ||||||
|     LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER); |     LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER); | ||||||
|  | |||||||
| @ -12,6 +12,8 @@ void furi_hal_speaker_init(); | |||||||
| 
 | 
 | ||||||
| void furi_hal_speaker_start(float frequency, float volume); | void furi_hal_speaker_start(float frequency, float volume); | ||||||
| 
 | 
 | ||||||
|  | void furi_hal_speaker_set_volume(float volume); | ||||||
|  | 
 | ||||||
| void furi_hal_speaker_stop(); | void furi_hal_speaker_stop(); | ||||||
| 
 | 
 | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 あく
						あく