Просмотр исходного кода

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>
あく 3 лет назад
Родитель
Сommit
f5175e1388

+ 6 - 1
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

+ 223 - 325
applications/music_player/music_player.c

@@ -2,132 +2,93 @@
 #include <furi_hal.h>
 
 #include <gui/gui.h>
-#include <input/input.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;
-
-typedef enum {
-    L1 = 1,
-    L2 = 2,
-    L4 = 4,
-    L8 = 8,
-    L16 = 16,
-    L32 = 32,
-    L64 = 64,
-    L128 = 128,
-} MelodyEventLength;
+#include <dialogs/dialogs.h>
+#include "music_player_worker.h"
 
-typedef struct {
-    MelodyEventNote note;
-    MelodyEventLength length;
-} MelodyEventRecord;
+#define TAG "MusicPlayer"
 
-typedef struct {
-    const MelodyEventRecord* record;
-    int8_t loop_count;
-} SongPattern;
-
-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}};
-
-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}};
-
-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}};
-
-const SongPattern song[] = {{melody_start, 1}, {melody_loop, -1}};
-
-typedef enum {
-    EventTypeTick,
-    EventTypeKey,
-    EventTypeNote,
-    // add your events type
-} MusicDemoEventType;
+#define MUSIC_PLAYER_APP_PATH_FOLDER "/any/music_player"
+#define MUSIC_PLAYER_APP_EXTENSION "*"
 
-typedef struct {
-    union {
-        InputEvent input;
-        const MelodyEventRecord* note_record;
-    } value;
-    MusicDemoEventType type;
-} MusicDemoEvent;
+#define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4
 
 typedef struct {
-    ValueMutex* state_mutex;
-    osMessageQueueId_t event_queue;
+    uint8_t semitone_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE];
+    uint8_t duration_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE];
 
-} MusicDemoContext;
+    uint8_t volume;
+    uint8_t semitone;
+    uint8_t dots;
+    uint8_t duration;
+    float position;
+} MusicPlayerModel;
 
-#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;
+    MusicPlayerModel* model;
+    osMutexId_t* model_mutex;
+
+    osMessageQueueId_t input_queue;
+
+    ViewPort* view_port;
+    Gui* gui;
+
+    MusicPlayerWorker* 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 "--";
+    }
+}
 
-    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,101 +98,33 @@ 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:
-        if(id == 3) return true;
-        break;
-    default:
-        break;
-    }
-
-    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-";
+static bool is_black_note(uint8_t semitone, uint8_t id) {
+    switch(semitone) {
+    case 1:
+        if(id == 0) return true;
         break;
-    case B5:
-        return "B5-";
+    case 3:
+        if(id == 1) return true;
         break;
-    case C6:
-        return "C6-";
+    case 6:
+        if(id == 3) return true;
         break;
-    case D6:
-        return "D6-";
+    case 8:
+        if(id == 4) return true;
         break;
-    case E6:
-        return "E6-";
+    case 10:
+        if(id == 5) return true;
         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;
-    }
+    return false;
 }
 
 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);
-
-    // 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);
+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];
     }
-    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;
-    }
+    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;
+            }
+            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);
+        }
 
-    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);
+        if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) {
+            FURI_LOG_E(TAG, "Unable to load file");
+            break;
+        }
 
-    // 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;
-    }
+        music_player_worker_start(music_player->worker);
 
-    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;
+        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]);
             }
-        } else {
-            // event timeout
+
+            osMutexRelease(music_player->model_mutex);
+            view_port_update(music_player->view_port);
         }
 
-        view_port_update(view_port);
-        release_mutex(&state_mutex, state);
-    }
+        music_player_worker_stop(music_player->worker);
+    } while(0);
 
-    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);
+    string_clear(file_path);
+    music_player_free(music_player);
 
     return 0;
 }

+ 46 - 0
applications/music_player/music_player_cli.c

@@ -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 - 0
applications/music_player/music_player_worker.c

@@ -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, &note_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 - 0
applications/music_player/music_player_worker.h

@@ -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);

+ 3 - 1
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

+ 6 - 0
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

+ 10 - 0
firmware/targets/f7/furi_hal/furi_hal_resources.c

@@ -1,4 +1,5 @@
 #include <furi_hal_resources.h>
+#include <furi_hal_delay.h>
 #include <furi.h>
 
 #include <stm32wbxx_ll_rcc.h>
@@ -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);

+ 38 - 16
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);

+ 2 - 0
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